Files
Arcrun/.claude/hooks/pre-write-guard.sh
T
Leo 202a5ab8d6 feat(registry): Phase 3 零件投稿靜態把關 + component-gatekeeping SDD
新 SDD .agents/specs/component-gatekeeping/(richblack 確認,含 venue 修訂 + 信任模型)。

registry 端靜態把關(CF Worker 可跑,不執行 wasm):
- G1 detectFakeComponent: 外部 URL/domain + http_request 子集偵測,硬擋退稿指回 recipe
- G3 wasmImports: 解析 wasm import section,只准 wasi_snapshot_preview1 + u6u 白名單
- G5/G6: unimplemented_steps 明列 gherkin/cold_start/runtime_compat,不假綠(§3c/§7)
- gherkin_evidence 一致性驗證(投稿者本地跑,registry 不重跑——CF 禁 runtime 編譯 wasm)

把關範圍:公共庫 + self-hosted 私人庫同一套(design §0.0)。
信任模型(design §4.5):Gherkin 全綠≠安全;純 WASI 沙箱框死能力才是發佈底氣;
第一期 evidence 可造假(誠實標明),平台重跑列未來。

hook: pre-write-guard 白名單加 component-gatekeeping / component-registry-canon SDD 目錄。

測試: sandboxAcceptance.test.ts 4 綠(含 G1 假零件被擋)。

待續(同 SDD): G4 CLI 投稿指令本地跑 Gherkin、G0 人類閘門、R5 白名單+本機 hook。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 17:53:03 +08:00

193 lines
9.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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: contentEdit: new_stringMultiEdit: 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.1registry/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.2cypher-executor TS 裡不准實作業務邏輯(只准 wasi-shim.ts 做 crypto
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$FILE_PATH" == *"cypher-executor/src/"* && "$FILE_PATH" == *.ts ]]; then
BASE=$(basename "$FILE_PATH")
# 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 職責
# 例外:auth-recipe-seeds.ts 是 recipe 資料定義(會被序列化寫進 RECIPES KV),
# 其中的 {{secret.X}} / {{runtime.X}} 是「資料字面值」而非 TS 展開邏輯,
# 真正的展開仍在 WASM auth primitive 內完成。
if [[ "$BASE" != "auth-recipe-seeds.ts" ]] && echo "$CONTENT" | grep -qE "\{\{(secret|runtime)\." ; then
block "2.2" \
"Template 展開({{secret.X}} / {{runtime.X}})屬於 WASM auth primitive 職責" \
"把這段邏輯改寫到 registry/components/auth_static_key/main.goTinyGo"
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 實作
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
if [[ "$BASE" != "wasi-shim.ts" ]]; 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
# ─────────────────────────────────────────────────────────────────────────────
# 規則 4.3:禁止自行在 .agents/specs/ 下建新 SDD 目錄
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$FILE_PATH" == *".agents/specs/"* ]]; then
# 檢查是否在已知 SDD 目錄內
KNOWN_SDDS=(
".agents/specs/arcrun"
".agents/specs/u6u-core-mvp"
".agents/specs/u6u-platform-evolution"
".agents/specs/component-registry-canon"
".agents/specs/component-gatekeeping" # 2026-05-29 richblack 確認新建(Phase 3 把關)
)
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" \
"禁止自行在 .agents/specs/ 下建立新的頂層 SDD 目錄" \
"先與 richblack 確認 SDD 範圍。若是現有 SDD 的補充檔案,請放到已知 SDD 目錄下"
fi
fi
exit 0