iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
AI Engineering

打造具備記憶與執行能力的常駐 AI Agent:Hermes Agent × Gemini × MCP 的 Harness 設計系列 第 26 篇

【Day 26】同一支私鑰,換個路徑寫法就從放行變成要確認

  • 分享至 

  • xImage
  •  

前面幾天把工具一個個掛上去,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:偵測器實際拿去比對的字串。指令從模型送出到判定之間經過一層改寫,這個欄位把改寫後的樣子攤開來

兩張表,一張 12 條一張 99 條

判定依據放在 tools/approval.py,兩張表分工明確:

  • HARDLINE_PATTERNS:12 條,命中就是 hardline-deny,任何繞過開關都無效
  • DANGEROUS_PATTERNS:99 條,命中轉成 ask-approval,交給人決定

硬性那 12 條按它們的描述字串分成四群:

  • 遞迴刪除:根目錄、系統目錄、家目錄各一條
  • 清除儲存裝置:mkfs 格式化、dd 寫入裸區塊裝置、重導向到裸區塊裝置
  • 行程:fork bomb、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,同一個樣式要被核准兩次以上才列入提議
  • 破壞性類別永久排除,遞迴刪除、sudo、磁碟寫入、憑證編輯這幾類無論核准幾次都提不出來

這個指令產出的是「常做又相對安全」的那一段,危險類別留在每次都問的狀態。整份提議列印出來之後要再帶 --apply 指定編號才寫進組態,預設只看不改。

白名單的來源是已經發生過的核准決定,它記錄的是人反覆做過的同一個判斷


心得

兩個指令的差別只有使用者名稱,一個放行一個要確認。第一次看到的反應是測試方式寫錯了,於是把兩次的 JSON 並排看,差別落在 normalized_variants 那一欄:一個是 ~/.ssh/id_rsa,另一個是 C:/Users/alice/.ssh/id_rsa。

規則認的是 users\<name>\.ssh 這串字,正規化先把它換成 ~/,規則拿到的輸入裡已經沒有可以命中的東西。兩段程式各自都對,接起來之後覆蓋範圍少了一塊。

這個欄位本來看起來只是除錯用的附帶資訊。它是唯一能看出改寫發生在哪一步的地方,把它印出來的那個決定,比規則表本身更能說明判定為什麼是這個結果。

一條規則實際擋得住什麼,要看送到它面前的字串長什麼樣


明天

工具呼叫留下紀錄的三種做法,以及各自看得到的範圍。


上一篇
【Day 25】打開 mcp serve 以為會拿到 agent,拿到的是一座訊息橋
系列文
打造具備記憶與執行能力的常駐 AI Agent:Hermes Agent × Gemini × MCP 的 Harness 設計 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言