46afea83c2
管道本來就是好的(install-harness 功能完整、冪等),**過時的是內容**:
harness skill(4066B)grep「意圖」「>>」= 0 命中,只講世界觀/別寫 Python
⇒ 新裝的封測者拿不到步驟 1 的核心教材(`>>` 意圖語法)。
■ 單一真相源:harness skill 改為建置期由 registry 複製
build-harness-skill.mjs=head + registry/skills/write_intent_workflow.md 正文 + tail。
選「建置期複製」的理由:npm files 只收 harness/,registry 不進套件;
symlink 在 npm pack 與 Windows 不可靠。產物 commit 進 repo(npm 裝的是產物、不跑 build)。
head/tail 是 harness 專屬(CLI 語境入口/acr 指令表/暴露同意/誠實鐵律),
install-harness 的 copyTree 跳過 .head/.tail,不鋪進使用者專案。
■ 其餘三件逐份對照現世代事實後更新(過時的直接刪,不留死代碼)
- CLAUDE.block.md:補 >> 意圖語法、not_found 兩條路、零件 vs recipe 分型、
腹語術紅線、金鑰只拿名字
- commands/arcrun.md:步驟改成「先寫意圖 → 丟去查 → 再寫 YAML」,補 acr search/validate
- hooks/arcrun-guard.sh:**正路提示改為指向 arcrun-mindset Skill +意圖語法**
(呼應「hook 沒提 skill 反而把 AI 導向 repo 文件」的教訓);
新增 code 節點腹語術提醒,settings.fragment 補 Write|Edit|MultiEdit matcher
■ 世代閘(防再度脫節)
check-harness-generation.mjs 檢查四件交付物的現世代指紋,缺指紋 exit 1,
掛進 npm run build(prepublishOnly 因此也擋)。
反向驗證:把 skill/CLAUDE.block 換回上一代 → 兩者都被擋下並逐條點名缺哪個指紋。
■ 驗收(考生 haiku/受測物=環境)
乾淨臨時目錄 acr install-harness → 四件鋪好;重跑冪等(全檔 md5 不變、
CLAUDE.md 66 行不變、hooks 條目 2 不變、arcrun 區塊仍 1 個)。
haiku 只讀該目錄的 CLAUDE.md+SKILL.md(明令禁讀 ~/.claude、禁上網;
兩份教材 md5 與大小均不同,可證非考本機那支)答十題
→ grade-step1.sh **10 / 10 通過**(判分器同時反向驗證仍會抓
ON_TRUE/ON_FAILURE/第一節點非 input)。
npm test 18/18、tsc 綠。
SDD:workflow-discovery/tasks.md 3.11
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
105 lines
7.4 KiB
Bash
105 lines
7.4 KiB
Bash
#!/bin/bash
|
||
# arcrun-guard.sh — 用戶專案的 arcrun PreToolUse guard(由 acr install-harness 裝進 .claude/hooks/)
|
||
#
|
||
# 對象:在「用 arcrun 開發」的專案裡工作的 CC。擋它走歪(退回自寫 Python / 不用 recipe / 未經同意暴露)。
|
||
# 與 arcrun repo 開發版 hook 完全不同(那個擋的是開發 arcrun 本身)。
|
||
#
|
||
# 鐵則(user-cc-harness design §0.5):每次擋下/提醒都要給「具體怎麼做才對」的正路,不只說「不行」。
|
||
#
|
||
# 退出碼:0=允許(可附 stderr 提醒);2=硬擋(stderr 回給 CC)。
|
||
# 分級(design §4):多數用「提醒不硬擋」(避免誤殺正常 python);硬擋只留給「未經同意暴露資料」。
|
||
|
||
set -o pipefail
|
||
INPUT=$(cat)
|
||
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
||
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
||
|
||
remind() {
|
||
# 提醒但放行(exit 0)。CC 看到 stderr,自己判斷是否真要繼續。
|
||
echo "💡 arcrun 提醒:$1" >&2
|
||
echo " 正路:$2" >&2
|
||
exit 0
|
||
}
|
||
block() {
|
||
echo "❌ arcrun guard 擋下:$1" >&2
|
||
echo " 正路:$2" >&2
|
||
exit 2
|
||
}
|
||
|
||
# ── 硬擋:未經人類同意的暴露動作(明確越界,mindset §5)──────────────
|
||
# 非互動環境下 CC 自己跑「部署對外 webhook / push recipe」= 替人類決定公開。
|
||
#
|
||
# 壓測 §9.5 修正(A6):舊版 grep "acr push" 連「在 echo/heredoc 裡**提到** push 字串」
|
||
# 都擋 → CC 連「把可貼上的指令印給使用者看」都做不到,反而違反 §0.5「擋下必指正路」鐵則。
|
||
# 故先判斷此命令是「展示指令」還是「真的執行 push」,只擋後者。
|
||
#
|
||
# 誠實限制(mindset §7):shell 命令層**無法 100% 乾淨區分**「執行」與「展示」
|
||
# (例:`echo "跑 acr push"` 與 `acr push` 對單純 grep 都命中)。以下是**啟發式**,
|
||
# 目的是降低誤殺、讓「印出正路指令」這條合法用途能通,不是完美防護。邊角情況可能漏判,
|
||
# 但漏判方向是「偏向放行展示」——而真正的暴露(執行)仍受 TTY/env 把關。
|
||
if echo "$CMD" | grep -qE "acr (push|recipe push)\b"; then
|
||
# 目標(壓測 §9.5):擋「真的執行 push」,放行「把 push 指令印給使用者看」。
|
||
#
|
||
# 作法:把 heredoc 主體(cat/印出用的多行文字)先抽掉,剩下的才是「實際會被 shell 執行的命令列」,
|
||
# 再看 push 是否出現在那裡的「命令位置」(行首 / ; & | && || 之後)。
|
||
# 執行 → 命中:`acr push x`、`cd f && acr push x`、`echo done; acr push x`
|
||
# 展示 → 不命中:`echo "跑 acr push x"`、`cat <<EOF\n acr push x \nEOF`(push 在 heredoc 主體內)
|
||
#
|
||
# 抽掉 heredoc 主體:刪掉從 `<<EOF`(或任何 <<TOKEN)那行的下一行起、到單獨一行 TOKEN 為止的內容。
|
||
# 用 awk 做簡單狀態機(只處理最常見的 `<<TOKEN` / `<<-TOKEN`,不含複雜巢狀——夠用且誠實)。
|
||
EXEC_PART=$(printf '%s\n' "$CMD" | awk '
|
||
BEGIN{inhd=0}
|
||
{
|
||
if (inhd) { if ($0 ~ "^[[:space:]]*" term "[[:space:]]*$") { inhd=0 }; next }
|
||
line=$0
|
||
if (match(line, /<<-?[[:space:]]*"?'"'"'?[A-Za-z_][A-Za-z0-9_]*"?'"'"'?/)) {
|
||
t=substr(line, RSTART, RLENGTH); gsub(/^<<-?[[:space:]]*["'"'"']?/, "", t); gsub(/["'"'"']$/, "", t)
|
||
term=t; inhd=1
|
||
}
|
||
print line
|
||
}')
|
||
|
||
# 誠實限制(mindset §7):shell 層無法 100% 乾淨區分「執行」與「展示」。awk heredoc 抽取只覆蓋
|
||
# 常見形式(`<<EOF` / `<<-EOF` / 引號 token);`bash <<EOF ... acr push` 這種「heredoc 內容其實
|
||
# 會被執行」的偷渡,這裡會誤放——但它已被上游安全分類器擋下(壓測 §9.6 實證)。取捨上偏向
|
||
# 「放行展示」以保住 §0.5「擋下也要能印出正路指令」這條合法用途,不假裝此啟發式完美。
|
||
if echo "$EXEC_PART" | grep -qE "(^|[;&|][[:space:]]*)acr[[:space:]]+(push|recipe[[:space:]]+push)\b"; then
|
||
if [ ! -t 0 ] && [ "${ARCRUN_HUMAN_CONFIRMED:-}" != "1" ]; then
|
||
block "在非互動環境自動執行暴露動作(acr push / recipe push 會讓東西可被外部呼叫)" \
|
||
"交人類在終端機執行(真 TTY 會自動放行)。可把指令完整複製給使用者貼上自己跑:\`acr push <你的 workflow.yaml>\`。或使用者先在對話明示同意後親自於終端機執行。不要替使用者決定公開。(部署前的正路見 arcrun-mindset Skill:先 \`acr validate\`)"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ── 提醒(不硬擋):退回自寫 Python/Node 一次性自動化 ──────────────────
|
||
# 「我先用 Python 測試」這類退回熟悉工具的傾向。python 不絕對錯(可能跑測試),故提醒不擋。
|
||
if echo "$CMD" | grep -qE "(^|[;&| ])(python3?|node)[ ]+[^ ]+\.(py|js|mjs|ts)\b"; then
|
||
# 排除明顯的測試 / 既有工具呼叫(pytest / npm test / jest 等)降低誤判
|
||
if ! echo "$CMD" | grep -qE "(pytest|jest|vitest|npm (run )?test|mocha|\btest_)"; then
|
||
remind "偵測到用 python/node 跑腳本。這專案用 Arcrun,串服務/自動化不要自刻一次性腳本。" \
|
||
"讀 arcrun-mindset Skill,先把需求寫成「意圖」串(\`input >> ON_SUCCESS >> <下一步>\`,邊只有 ON_SUCCESS 與「對每個 X」),再用 \`acr search <關鍵字>\` 查哪些零件/recipe 存在,最後才寫 workflow.yaml → \`acr validate\` → \`acr run\`。若這確實不是自動化(例如跑測試/別的工具),忽略本提醒。"
|
||
fi
|
||
fi
|
||
|
||
# ── 提醒(不硬擋):自寫打固定 API 的 script,而非 recipe ──────────────
|
||
if echo "$CMD" | grep -qE "(curl|fetch|requests\.(get|post)|axios).*https?://"; then
|
||
remind "偵測到自己打外部 API。Arcrun 裡「打固定 endpoint」應寫成 recipe,不自刻 HTTP 呼叫。" \
|
||
"先 \`acr recipe search <服務名>\` 看有沒有現成的;沒有就自己寫幾行 YAML(canonical_id/endpoint/method/auth_service)用 \`acr recipe push\` 投稿,workflow 裡用 \`http_request\` + 該 recipe 引用它。缺 recipe 就自己補,不必問人。寫法見 arcrun-mindset Skill。"
|
||
fi
|
||
|
||
# ── 提醒(不硬擋):把 code 節點當成缺零件的替代品(「腹語術」)──────────────
|
||
# 查詢回 not_found 就改寫成 code = 表面用 Arcrun、實際全寫 JS。這是現世代最常見的走歪。
|
||
if [ "$TOOL" = "Write" ] || [ "$TOOL" = "Edit" ] || [ "$TOOL" = "MultiEdit" ]; then
|
||
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
||
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""')
|
||
if echo "$FILE" | grep -qE '\.(ya?ml)$' && echo "$CONTENT" | grep -qE 'component:[[:space:]]*["'"'"']?code\b'; then
|
||
# 只在 code 內容看起來在做流程控制/取代零件時提醒(含 if/for/fetch),單純整形不吵
|
||
if echo "$CONTENT" | grep -qE '\b(if[[:space:]]*\(|for[[:space:]]*\(|fetch\(|await[[:space:]]+fetch)'; then
|
||
remind "workflow 裡的 \`code\` 節點含流程控制/HTTP 呼叫——這可能是「腹語術」(表面用 Arcrun、實際全寫 JS)。" \
|
||
"\`code\` 只用於局部整形(例:剝掉 LLM 回應的雜訊、切段落)。缺外部 API → 寫 recipe(\`acr recipe push\`);缺計算能力 → 投稿零件 PR;要判斷 → 寫成獨立節點接 \`ON_SUCCESS\`(引擎沒有條件邊)。每個寫進 code 的 if 都是沒被測過的新 bug。見 arcrun-mindset Skill。"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
exit 0
|