#!/bin/bash # role-lib.sh — 兩軸身分判定函式庫(被 source,不獨立掛 hook) # # 出處:《分離導入規格》第二節「兩軸身分(scope × role,全部機械判定,agent 不自我判斷)」 # +《JDD 導入規格》第四節「角色與權限」。 # # ── 為什麼是「兩軸」而不是「一個角色設定」────────────────────── # scope(我在誰的地盤)=**安裝位置**決定:agent 在哪個資料夾醒來, # 就只讀得到那部憲法。來源= system-dev/.profile(install 時寫死)。 # role (我是什麼工種)=**環境變數 AGENT_ROLE** 決定: # orchestrator 效忠 root+journeys、禁寫 code 禁改 tasks; # engineer 效忠 requirements、禁改考卷(journeys/root/Gherkin)。 # # 兩軸都有機械來源 ⇒ **沒有任何一格是靠 agent 自陳的**。 # 這是整套封路的地基:agent 說自己是誰不算數,檔案和環境說了才算。 # # ── 身分矩陣(注意右下角那個空格)────────────────────────── # AGENT_ROLE=orchestrator AGENT_ROLE=engineer # orchestrator profile PM 本尊 ✅ 總管 repo 裡的技術 subagent ✅ # repo profile ❌ 不存在(擋) 寫 code 的 subagent ✅ # # 左下角為什麼要擋而不是靜默降級:成員 repo 裡沒有 PM。 # 靜默降級=把一個設定錯誤變成「看起來正常但權限不對」,那比報錯危險。 # 空格也是封路(分離導入規格 §2)。 # # ── 為什麼是函式庫不是六支各自為政的 hook ──────────────────── # 六支封路 hook 都要做同樣四件事:解析 JSON 取 file_path、算 repo 相對路徑、 # 判 scope、判 role。各寫一份=四處維護同一條規則,正是分離導入規格 §6.4 # 要避免的「兩處維護同一條規則」。 # # 用法(在 hook 裡): # source "$(dirname "${BASH_SOURCE[0]}")/lib/role-lib.sh" # INPUT="$(cat)" # FILE_PATH="$(sdt_file_path "$INPUT")" # sdt_assert_identity || exit 2 # # 誠實限制:這裡判的是「宣告出來的身分」。有人硬改 .profile 或亂設 AGENT_ROLE # 一樣會通過——它擋的是**意外與疏忽**,不是刻意繞道。留痕可審,不宣稱防偽。 # 注意:本檔被 source,**不要**在這裡 set -euo pipefail #(會把呼叫端的 shell 選項一起改掉,hook 的容錯行為會跟著變)。 # ── repo 根目錄 ──────────────────────────────────────── # 從本檔位置往上推三層:/.claude/hooks/lib/role-lib.sh # 為什麼不用 $CLAUDE_PROJECT_DIR:雲端/CI/被 source 進別的腳本時不保證存在 #(分離導入規格 §3:封路 hook 工具箱要「環境偵測、repo 相對路徑,雲端可跑」)。 sdt_repo_root() { local libdir libdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" (cd "$libdir/../../.." && pwd) } # ── 路徑正規化:絕對或相對 → repo 相對 ────────────────── sdt_rel_path() { local file_path="$1" root root="$(sdt_repo_root)" case "$file_path" in "$root"/*) printf '%s' "${file_path#"$root"/}" ;; /*) printf '%s' "$file_path" ;; # 別的地方的絕對路徑,原樣回(呼叫端自己判要不要管) *) printf '%s' "$file_path" ;; # 已是相對路徑 esac } # ── scope 軸:安裝位置決定 ────────────────────────────── # 讀不到 .profile 時回 repo:多數實例是成員 repo,而且此時 orchestrator 專屬的 # 攔截本來就不該觸發(總管禁入成員 repo 那條只裝在 orchestrator profile)。 # 這是「最小驚訝」而非「最小權限」——選擇的代價寫在這裡,別假裝沒有。 sdt_scope() { local f v f="$(sdt_repo_root)/system-dev/.profile" if [ -f "$f" ]; then v="$(tr -d '[:space:]' < "$f" 2>/dev/null || true)" case "$v" in repo|orchestrator) printf '%s' "$v"; return 0 ;; esac fi printf 'repo' } # ── role 軸:AGENT_ROLE 環境變數決定;沒設就依 scope 推定 ── sdt_role() { case "${AGENT_ROLE:-}" in orchestrator|engineer) printf '%s' "$AGENT_ROLE"; return 0 ;; esac # 未設定 → 依 scope 推定(不問 agent、不猜) case "$(sdt_scope)" in orchestrator) printf 'orchestrator' ;; *) printf 'engineer' ;; esac } # ── 矩陣檢查:擋掉不存在的那一格 ──────────────────────── # 回傳 0 = 身分合法;回傳 2 = 非法(呼叫端應 exit 2) sdt_assert_identity() { local scope role scope="$(sdt_scope)" role="$(sdt_role)" if [ "$scope" = "repo" ] && [ "$role" = "orchestrator" ]; then cat >&2 </dev/null 2>&1; then printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null && return 0 fi if command -v python3 >/dev/null 2>&1; then printf '%s' "$input" | python3 -c ' import json,sys try: d=json.load(sys.stdin) print(d.get("tool_input",{}).get("file_path","") or "") except Exception: print("") ' 2>/dev/null && return 0 fi printf '%s' "$input" \ | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' \ | head -1 \ | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//' } # ── 從 hook 的 JSON 取要寫入的內容(Write 用 content、Edit 用 new_string)── sdt_write_content() { local input="$1" [ -z "$input" ] && return 0 if command -v jq >/dev/null 2>&1; then printf '%s' "$input" \ | jq -r '[.tool_input.content, .tool_input.new_string] | map(select(. != null)) | join("\n")' 2>/dev/null \ && return 0 fi if command -v python3 >/dev/null 2>&1; then printf '%s' "$input" | python3 -c ' import json,sys try: ti=json.load(sys.stdin).get("tool_input",{}) print("\n".join(x for x in (ti.get("content"), ti.get("new_string")) if x)) except Exception: print("") ' 2>/dev/null && return 0 fi # 無 jq/python3:退回整包(寧可多掃不漏掃,沿 wiki-secret-scan.sh 的慣例) printf '%s' "$input" } # ── 除錯用:一行印出目前身分 ──────────────────────────── sdt_identity_line() { printf 'scope=%s role=%s root=%s' "$(sdt_scope)" "$(sdt_role)" "$(sdt_repo_root)" }