259 lines
15 KiB
Bash
Executable File
259 lines
15 KiB
Bash
Executable File
#!/bin/bash
|
||
# .claude/hooks/pre-write-guard.sh
|
||
# arcrun PreToolUse guard for Write / Edit / MultiEdit
|
||
#
|
||
# 職責:擋下會違反 CLAUDE rules 的檔案寫入操作
|
||
# 退出 code:
|
||
# 0 = 允許
|
||
# 2 = 擋下(stderr 訊息會回傳給 CC)
|
||
#
|
||
# 依賴:jq
|
||
|
||
set -o pipefail
|
||
|
||
INPUT=$(cat)
|
||
|
||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
|
||
# 取得將要寫入的內容(Write: content;Edit: new_string;MultiEdit: edits[].new_string 全部串起來)
|
||
CONTENT=$(echo "$INPUT" | jq -r '
|
||
.tool_input.content
|
||
// .tool_input.new_string
|
||
// (.tool_input.edits // [] | map(.new_string // "") | join("\n"))
|
||
// ""
|
||
')
|
||
|
||
block() {
|
||
local rule="$1"
|
||
local reason="$2"
|
||
local fix="$3"
|
||
cat >&2 <<EOF
|
||
❌ BLOCKED by arcrun CLAUDE rules
|
||
違反項:${rule}
|
||
檔案:${FILE_PATH}
|
||
原因:${reason}
|
||
正確做法:${fix}
|
||
參考:.claude/rules/02-forbidden.md
|
||
EOF
|
||
exit 2
|
||
}
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# 規則 1.1:registry/components/ 下不准 TS(除非是 AssemblyScript)
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
if [[ "$FILE_PATH" == *"registry/components/"* && "$FILE_PATH" == *.ts ]]; then
|
||
# 允許 asconfig.json 同目錄的 AssemblyScript
|
||
COMP_DIR=$(dirname "$FILE_PATH")
|
||
if [[ ! -f "$COMP_DIR/asconfig.json" ]]; then
|
||
block "1.1" \
|
||
"registry/components/ 下禁止 TypeScript(除非是 AssemblyScript 且同目錄有 asconfig.json)" \
|
||
"零件必須用 TinyGo (main.go) 或 AssemblyScript 實作並編譯成 .wasm"
|
||
fi
|
||
fi
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# 規則 1.2:禁止在非法位置新增 auth/credential 實作
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# 合法位置:registry/components/auth_static_key | auth_oauth2 | auth_service_account | auth_mtls
|
||
if [[ "$FILE_PATH" =~ auth[-_](static[-_]key|oauth2|service[-_]account|mtls) ]]; then
|
||
if [[ "$FILE_PATH" != *"registry/components/auth_"* ]]; then
|
||
block "1.2" \
|
||
"auth primitive 實作只能放在 registry/components/auth_<type>/" \
|
||
"改去 registry/components/auth_static_key/ 等目錄,用 TinyGo 實作 main.go"
|
||
fi
|
||
fi
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# 規則 2.1:禁止新增含特定關鍵字的 TS 檔案(credential-injector / jwt-signer 等)
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
if [[ "$FILE_PATH" == *.ts ]]; then
|
||
BASE=$(basename "$FILE_PATH")
|
||
# 既有的 credential-injector.ts / jwt-signer.ts 允許修改(為了刪除),但不准新增同名
|
||
if [[ "$BASE" =~ ^(credential[-_]injector|jwt[-_]signer)\.ts$ ]]; then
|
||
if [[ ! -f "$FILE_PATH" ]]; then
|
||
block "2.1" \
|
||
"禁止新增 ${BASE}(Phase 1-3 的目標是刪除此類檔案,不是重建)" \
|
||
"credential 注入 / JWT signing 屬於 WASM 零件職責,改去 registry/components/auth_*/"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# 規則 2.2:cypher-executor TS 裡不准實作業務邏輯(只准 wasi-shim.ts 做 crypto)
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
if [[ "$FILE_PATH" == *"cypher-executor/src/"* && "$FILE_PATH" == *.ts ]]; then
|
||
BASE=$(basename "$FILE_PATH")
|
||
|
||
# 通用類別:種子資料檔(*-seeds.ts)= installer 要灌進 KV 的 recipe 種子清單。
|
||
# 其中的 endpoint / {{template}} 字串是「資料欄位」(宣告 recipe 打哪 / 怎麼注入),
|
||
# 會被序列化寫進 RECIPES KV,**不是** TS 裡的呼叫 / 展開實作;真正執行仍走零件 / WASM primitive。
|
||
# 故整類豁免 endpoint / template 字串檢查。這是「種子資料檔」這個普遍類別的規則,
|
||
# 不是為某個零件 / recipe 開的特例(richblack 2026-06-06 原則:不為單一零件改全域規則)。
|
||
IS_SEED_DATA_FILE=false
|
||
if [[ "$BASE" == *-seeds.ts ]]; then
|
||
IS_SEED_DATA_FILE=true
|
||
fi
|
||
|
||
# crypto.subtle.decrypt:只准在 wasi-shim.ts
|
||
if echo "$CONTENT" | grep -qE "crypto\.subtle\.decrypt"; then
|
||
if [[ "$BASE" != "wasi-shim.ts" ]]; then
|
||
block "2.2" \
|
||
"AES-GCM 解密(crypto.subtle.decrypt)只准出現在 wasi-shim.ts 的 crypto_decrypt host function" \
|
||
"把解密邏輯移到 wasi-shim.ts 的 host function;或讓 WASM 零件透過 u6u.crypto_decrypt 呼叫"
|
||
fi
|
||
fi
|
||
|
||
# crypto.subtle.sign with RSASSA:只准在 wasi-shim.ts
|
||
if echo "$CONTENT" | grep -qE "crypto\.subtle\.sign.*RSASSA"; then
|
||
if [[ "$BASE" != "wasi-shim.ts" ]]; then
|
||
block "2.2" \
|
||
"RS256 簽章只准出現在 wasi-shim.ts 的 crypto_sign_rs256 host function" \
|
||
"把簽章移到 wasi-shim.ts;或讓 auth_service_account WASM 透過 u6u.crypto_sign_rs256 呼叫"
|
||
fi
|
||
fi
|
||
|
||
# Template 展開:{{secret.X}} 或 {{runtime.X}} 屬於 WASM 職責
|
||
# 例外:種子資料檔(*-seeds.ts)裡的 {{secret.X}} / {{runtime.X}} 是「資料字面值」而非 TS 展開邏輯,
|
||
# 真正的展開仍在 WASM auth primitive 內完成。
|
||
if [[ "$IS_SEED_DATA_FILE" == "false" ]] && echo "$CONTENT" | grep -qE "\{\{(secret|runtime)\." ; then
|
||
block "2.2" \
|
||
"Template 展開({{secret.X}} / {{runtime.X}})屬於 WASM auth primitive 職責" \
|
||
"把這段邏輯改寫到 registry/components/auth_static_key/main.go(TinyGo)"
|
||
fi
|
||
|
||
# Hard-code 的 BUILTIN_API_RECIPES / BUILTIN_CREDENTIALS_MAP 新增
|
||
if echo "$CONTENT" | grep -qE "(BUILTIN_API_RECIPES|BUILTIN_CREDENTIALS_MAP)\s*[:=]"; then
|
||
# 允許「把它設成空物件」或「刪除」,但不准新增實作
|
||
if echo "$CONTENT" | grep -qE "BUILTIN_API_RECIPES.*=.*\{\s*[a-zA-Z]"; then
|
||
block "2.2" \
|
||
"禁止在 TS 裡新增 BUILTIN_API_RECIPES / BUILTIN_CREDENTIALS_MAP 實作" \
|
||
"API 呼叫邏輯屬於各自的 WASM 零件(gmail.wasm / telegram.wasm 等),cypher-executor 只做 routing"
|
||
fi
|
||
fi
|
||
|
||
# Hard-code API endpoint 實作
|
||
# 例外:種子資料檔(*-seeds.ts)的 endpoint 字串(sheets.googleapis.com 等)是 recipe 的
|
||
# 「資料欄位」(宣告這個 recipe 預設打哪),會被序列化寫進 RECIPES KV,**不是** TS 裡的呼叫實作;
|
||
# 真正的 API 呼叫仍走原本零件 / http_request 路徑。
|
||
HARDCODED_APIS=(
|
||
"gmail\.googleapis\.com/gmail/v1/users/me/messages/send"
|
||
"api\.telegram\.org/bot.*sendMessage"
|
||
"sheets\.googleapis\.com/v4/spreadsheets"
|
||
"notify-api\.line\.me/api/notify"
|
||
)
|
||
for PATTERN in "${HARDCODED_APIS[@]}"; do
|
||
if echo "$CONTENT" | grep -qE "$PATTERN"; then
|
||
# 允許 wasi-shim.ts(http_request host function proxy)+ 種子資料檔(*-seeds.ts)
|
||
if [[ "$BASE" != "wasi-shim.ts" && "$IS_SEED_DATA_FILE" == "false" ]]; then
|
||
block "2.2" \
|
||
"禁止在 cypher-executor TS 裡 hard-code API endpoint(偵測到: $PATTERN)" \
|
||
"把 API 呼叫移到對應的 WASM 零件(registry/components/gmail/main.go 等)"
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# exchangeGoogleJwt / 類似 token exchange function
|
||
if echo "$CONTENT" | grep -qE "(exchangeGoogleJwt|exchangeServiceAccountJwt|signGoogleJwt)"; then
|
||
if [[ "$BASE" != "wasi-shim.ts" ]]; then
|
||
block "2.2" \
|
||
"Token exchange 邏輯屬於 auth_service_account WASM 零件" \
|
||
"改到 registry/components/auth_service_account/main.go"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# 規則 3.3:禁止建立 *-v2 / new-* / *-worker 類複製貼上目錄
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
if [[ "$FILE_PATH" =~ /(auth|credential|jwt|oauth|gmail|telegram|google-sheets|line-notify|http-request)[-_](v2|v3|new|worker|backup|temp)/ ]]; then
|
||
block "3.3" \
|
||
"禁止為同一零件建立平行目錄(v2/new/worker/backup 等)" \
|
||
"直接修改 registry/components/<name>/main.go 即可;需要版本管理請用 git branch"
|
||
fi
|
||
if [[ "$FILE_PATH" =~ /new-(auth|credential|jwt|oauth|gmail|telegram)/ ]]; then
|
||
block "3.3" \
|
||
"禁止為同一零件建立 new-<name>/ 平行目錄" \
|
||
"直接修改 registry/components/<name>/main.go"
|
||
fi
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# 規則 7.x:薄殼原則(能力長在 API,介面只暴露)— 見 .claude/rules/07-thin-shell.md
|
||
# 只擋語法層可偵測的反例;recipe 層拼裝 / 藏在 helper 的邏輯需 code review 把關(07 §6)。
|
||
# 範圍:薄殼介面 = cli/src/ 與 arcrun-mcp/src/(不含 cypher-executor,那是 API 本體)。
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
block7() {
|
||
cat >&2 <<EOF
|
||
❌ BLOCKED by arcrun CLAUDE rules
|
||
違反項:7(薄殼原則)
|
||
檔案:${FILE_PATH}
|
||
原因:$1
|
||
正確做法:$2
|
||
參考:.claude/rules/07-thin-shell.md
|
||
EOF
|
||
exit 2
|
||
}
|
||
|
||
if [[ ( "$FILE_PATH" == *"/cli/src/"* || "$FILE_PATH" == *"arcrun-mcp/src/"* ) \
|
||
&& ( "$FILE_PATH" == *.ts || "$FILE_PATH" == *.js ) ]]; then
|
||
|
||
# 7.1 seed 邏輯不准寫在介面層(§4.1 反例)。seed 是 API 行為,由 API 端點完成。
|
||
# 允許薄殼「呼叫一個 seed 端點」(單一 fetch),但不准在介面層定義 seed 編排函式。
|
||
if echo "$CONTENT" | grep -qE "(function|const|async)\s+(seedApiRecipes|seedAuthRecipes|seedRecipes)\b"; then
|
||
block7 \
|
||
"seed 編排邏輯寫在介面層(CLI/MCP),這是壓測 §4.1 的根因——seed 應是 API 行為" \
|
||
"把 seed 改成 API 端點(cypher-executor POST /init/seed 或部署完成時自動 seed),薄殼只呼叫該端點一次"
|
||
fi
|
||
|
||
# 7.2 「先 update 失敗再 insert」/「先 GET 找有則 PATCH 無則 POST」這類 upsert 拼裝
|
||
# 在同一函式內同時出現 update/patch 與 insert/post 的條件分支 = 把 API 缺的能力拼在介面層。
|
||
if echo "$CONTENT" | grep -qiE "method:\s*['\"]PATCH['\"]" && echo "$CONTENT" | grep -qiE "method:\s*['\"]POST['\"]"; then
|
||
if echo "$CONTENT" | grep -qiE "(upsert|找到.*PATCH|沒找到.*POST|exists.*update.*else.*insert|if.*found.*patch)"; then
|
||
block7 \
|
||
"在介面層拼裝 upsert(先找/PATCH 否則 POST)——這是能力,該在 API 提供單一 upsert 端點" \
|
||
"請 cypher-executor 提供 upsert 端點(API 內部做 GET→PATCH/POST),薄殼只呼叫該端點"
|
||
fi
|
||
fi
|
||
|
||
# 7.3 client 端「全部成功才做下一步」這類補 API 缺口的 gate(§4.1 deployFullyOk 反例)
|
||
if echo "$CONTENT" | grep -qE "(deployFullyOk|全部成功.*test|/全部成功/)"; then
|
||
block7 \
|
||
"用 client 端字串判斷 gate 後續行為(deployFullyOk 類)——脆弱且把編排責任推給介面層" \
|
||
"讓 API 保證後置狀態(部署成功即 recipe 就緒),薄殼不做這種 gate"
|
||
fi
|
||
fi
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# 規則 4.3:禁止自行在 docs/3-specs/ 下建新 SDD 目錄
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
if [[ "$FILE_PATH" == *"docs/3-specs/"* ]]; then
|
||
# 檢查是否在已知 SDD 目錄內
|
||
KNOWN_SDDS=(
|
||
"docs/3-specs/arcrun"
|
||
"docs/3-specs/arcrun-core-mvp"
|
||
"docs/3-specs/arcrun-platform-evolution"
|
||
"docs/3-specs/component-registry-canon"
|
||
"docs/3-specs/component-gatekeeping" # 2026-05-29 richblack 確認新建(Phase 3 把關)
|
||
"docs/3-specs/data-exfil-warning" # 2026-05-30 richblack 確認新建(資料外流警示)
|
||
"docs/3-specs/user-cc-harness" # 2026-06-03 richblack 確認新建(用戶 CC harness 安裝)
|
||
"docs/3-specs/llm-interface" # 2026-05-16 richblack 確認新建(AI 操盤手介面)
|
||
"docs/3-specs/recipe-system" # richblack 確認新建(recipe 系統)
|
||
"docs/3-specs/resumable-workflow" # richblack 確認新建(可恢復工作流)
|
||
"docs/3-specs/workflow-discovery" # 2026-06-27 總管 issue #8 交辦新建(工作流 description slot + search_workflow,北極星入口缺口)
|
||
"docs/3-specs/thin-shell-alignment" # 2026-06-27 總管 issue #11 交辦新建(CLI/MCP 薄殼漂移全面盤點 + 防複發機制)
|
||
"docs/3-specs/portal-auth" # 2026-07-13 總管 issue #24/#25 交辦新建(RAG Portal 多人授權,rag-wave1 T1/T2)
|
||
)
|
||
IN_KNOWN=false
|
||
for K in "${KNOWN_SDDS[@]}"; do
|
||
if [[ "$FILE_PATH" == *"$K/"* ]]; then
|
||
IN_KNOWN=true
|
||
break
|
||
fi
|
||
done
|
||
if [[ "$IN_KNOWN" == "false" ]]; then
|
||
block "4.3" \
|
||
"禁止自行在 docs/3-specs/ 下建立新的頂層 SDD 目錄" \
|
||
"正確程序(見 .claude/rules/00-sdd-protocol.md「新增 SDD 的完整程序」):(1) 先與 richblack 確認要新建此 SDD;(2) 取得明確確認後,在本檔 KNOWN_SDDS 陣列加一行 \"docs/3-specs/<目錄名>\"(執行已授權任務的必要步驟);(3) 白名單放行後才寫 design/tasks。若是現有 SDD 的補充檔,改放已知 SDD 目錄下。"
|
||
fi
|
||
fi
|
||
|
||
exit 0
|