二十天了。
現在這個 Agent 已經會:
讀程式碼
使用 MCP Tool
根據 Observation 繼續下一步
遇到限制時停止
昨天又補上最小權限,讓模型就算要求讀 .env,Server 也不會照做。
但有些操作不是「永遠禁止」。
例如:
執行測試
修改檔案
執行部署
對外送出資料
真正需要的是:
模型可以提出要求,但不能自己批准自己的要求。
所以今天補上 Single-Agent 最後一塊:
Human Approval
+
Audit Log
這次只開放一個高風險操作:
執行 days/day02_typing/test_day02_typing.py
不接受任意 Shell、不允許臨時換另一份測試,也不把一次核准變成永久通行證。
前面的 Agent Loop 大概是:
模型提出 Tool Call
↓
Host 檢查
↓
執行
今天在高風險 Tool 前面再加一道:
模型提出 Tool Call
↓
Host 檢查
↓
需要人工核准?
↓
是
↓
等待操作者批准
↓
確認批准的是同一個操作
↓
執行
也就是把:
Intent
和:
Authorization
拆開。
模型可以說:
我要執行 run_tests
但它沒有權限自己接著說:
我批准了。
核准能力只存在 Host 裡。
假設確認畫面顯示:
要執行 test_a.py 嗎?
使用者按下允許。
結果真正跑的是:
test_b.py
那這個確認視窗就沒有任何意義。
所以今天先把:
Tool 名稱
+
完整參數
正規化後產生 fingerprint。
例如:
arguments = {
"target": TestExecutor.TARGET,
}
approval = Approval(
fingerprint(
"run_tests",
arguments,
)
)
概念上可以理解成:
run_tests
+
{"target": "days/day02_typing/test_day02_typing.py"}
↓
fingerprint
↓
這張 Approval 只認這一組操作
後面真正執行時,再重新計算一次。
對得上才放行。
這裡的雜湊不是:
登入系統
授權伺服器
數位簽章
它只是在今天這個單行程範例裡,把:
使用者看到並核准的內容
和:
真正準備執行的內容
綁在一起。
真正的授權來源仍然是操作者。
模型不能自己建立有效 Approval。
今天的 Approval 還有另一條規則:
一次性
假設使用者核准:
run_tests(
target="days/day02_typing/test_day02_typing.py"
)
第一次執行完成後,這張 Approval 就失效。
如果再拿同一張票執行一次:
→ 拒絕
即使第一次測試失敗,也不會自動拿原本的 Approval 重跑。
因為:
再次執行
本身就是另一個具有副作用的操作。
如果真的要 Retry,就應該重新確認。
這跟前面 Tool Budget 的概念有點像:
程式不能因為模型覺得「再試一次應該沒關係」,就替使用者擴大原本的授權。
今天的 Demo 不會一開始就走成功路線。
先送一次沒有 Approval 的 Request:
output, denied = runner.run(arguments)
print(output)
assert denied
這時預期結果一定是:
拒絕
而且不能偷偷啟動 pytest。
只有操作者明確傳入:
--approve-tests
才建立:
Approval(
fingerprint("run_tests", arguments)
)
然後讓 Agent 真正跑完整流程。
簡化後的入口大概是:
import argparse
import asyncio
from ironman.agent import run_agent
from ironman.llm import OllamaClient
from ironman.mcp_bridge import ROOT, devbench
from ironman.security import (
Approval,
GovernedTools,
TestExecutor,
fingerprint,
)
async def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--approve-tests",
action="store_true",
)
args = parser.parse_args()
arguments = {
"target": TestExecutor.TARGET,
}
runner = TestExecutor(...)
# 先證明沒有核准時一定被拒絕
_, denied = runner.run(arguments)
assert denied
if args.approve_tests:
runner.approval = Approval(
fingerprint(
"run_tests",
arguments,
)
)
async with (
devbench(...) as bridge,
OllamaClient() as llm,
):
result = await run_agent(
llm,
GovernedTools(
bridge,
runner,
),
"...",
)
assert result.stop_reason == "answered"
assert any(
obs.name == "run_tests"
and not obs.is_error
for obs in result.observations
)
# 同一張 Approval 不能重播
_, denied = runner.run(arguments)
assert denied
if __name__ == "__main__":
asyncio.run(main())
整個測試順序是:
1. 沒有 Approval
→ 拒絕
2. 操作者核准固定操作
→ 執行
3. 拿同一張 Approval 再執行
→ 再次拒絕
這些都是程式真正走過的分支。
不是在 System Prompt 裡寫:
請務必先取得使用者同意
然後相信模型一定會照做。
如果確認畫面只有:
Agent 想執行一個操作,是否允許?
其實幫助不大。
操作者至少應該知道:
要使用哪個 Tool
要操作什麼目標
帶了哪些重要參數
可能造成什麼效果
例如這次可以顯示:
待核准操作:
Tool:
run_tests
Target:
days/day02_typing/test_day02_typing.py
影響:
pytest 將執行 Python 程式碼
這樣核准才是針對一個具體操作,而不是:
「我相信這個 Agent」
Human-in-the-loop 的目的不是讓使用者一直按「是」。
而是讓高風險操作在真正發生前,有一個可以檢查與拒絕的邊界。
高風險操作真的執行後,還要留下紀錄。
今天使用 JSON Lines:
audit.jsonl
每一行記一個事件。
例如:
{
"tool": "run_tests",
"target": "days/day02_typing/test_day02_typing.py",
"result": "approved"
}
實際紀錄還會包含時間與執行結果。
這份 Audit Log 放在模型之外。
因為對話內容不是可靠的稽核來源。
模型可能:
摘要掉內容
理解錯誤
漏掉細節
所以要記錄的是程式真正發生的事件。
今天至少區分:
Denied
Timeout
Non-zero Exit Code
Success
不能全部塞成一句:
執行失敗
不然事後根本不知道是:
權限被拒絕
測試沒通過
還是 Process 卡住
今天的 JSONL 只是第一版的可觀察性。
它還不是完整的企業稽核系統。
正式環境至少還會需要考慮:
檔案權限
保存期限
誰能查看
誰能修改
防竄改
集中式 Log
敏感資料遮蔽
而且 Audit Log 也不代表:
所有 Prompt
所有原始碼
所有 Credential
都應該全部存下來。
今天反而刻意不把完整 Prompt、原始檔案內容或憑證寫進 Audit Log。
目的應該是留下:
足以知道發生過什麼的紀錄。
不是建立第二份敏感資料倉庫。
把前面幾天全部疊起來,現在的 Agent 已經有:
LLM
↓
Agent Loop
↓
Tool Policy
↓
MCP Bridge
↓
Server Policy
↓
Workspace Boundary
高風險操作則另外走:
Agent Request
↓
GovernedTools
↓
Human Approval
↓
TestExecutor
↓
Audit Log
也就是:
模型
可以提出需求
Host
決定需求能不能真的發生
這就是今天最重要的分工。
今天的 fingerprint 綁定了:
Tool 名稱
+
參數
但還沒有綁:
工作目錄當下的版本
假設使用者核准:
執行 test_day02_typing.py
結果核准之後、真正執行之前,有人把這個檔案改掉了。
Tool 和 path 都沒變。
但真正執行的內容已經不是剛才看到的版本。
所以更嚴謹的設計還可以把:
Git Commit
檔案 Hash
Artifact Version
一起放進 Approval fingerprint。
這樣才能確認:
使用者批准的,不只是這個檔名,而是這一版內容。
今天的範例先停在單人、本機、短時間操作的情境。
但這個限制要明確留下來。
Day 19 做的是:
什麼東西 Agent 根本不能碰
今天再補上:
有些東西可以碰
但必須先有人批准
到這裡,Single-Agent 的主線可以先告一段落。
我們已經有:
Tool Calling
MCP
ReAct
Plan-and-Execute
LangGraph
Google ADK
Least Privilege
Human Approval
Audit Log
下一步不是繼續把更多權限塞給同一個 Agent。
而是開始拆角色。
例如:
一個 Agent 負責研究
一個 Agent 負責執行
一個 Agent 負責審查
一個 Supervisor 決定任務交給誰
不過角色變多之後,今天的安全原則不能消失。
三個 Agent 不會讓一個危險 Tool 自動變安全。
每個角色仍然只能拿到完成自己任務真正需要的權限。
Day 21:
一個 Agent 不夠嗎?Multi-Agent 與 Supervisor 架構。