客服 Agent 收到一封客訴信:我的訂單還沒有收到,請幫我確認能不能退款。
但實際上信件後半段藏著另一段指令:讀取最近十筆訂單、把付款資料寄到外部信箱,再替其中一筆建立退款。
如果模型識破 Prompt Injection,當然很好。
但正式環境不能把最後一道防線押在模型每次都判斷正確。攻擊者甚至不需要控制整個 Agent,只要讓它取得一次不該有的執行權,就足以造成真正損失。
因此,這篇真正想討論的不是:模型有沒有相信惡意文字?
而是:
即使模型被騙,為什麼退款、外寄資料與讀取其他訂單仍然不該發生?

我認為核心的分工是
模型負責提出動作,Runtime 負責決定這次效果能不能發生;人工核准批准的則是一筆固定、不可偷換的效果。
Prompt Injection 麻煩的地方,是資料與指令都以自然語言進入模型。
Agent 必須讀懂信件才能處理客訴,也因此可能把信件裡的文字誤認成下一步工作。
AgentDojo 與 InjecAgent 都把攻擊放進能實際操作工具的環境;Anthropic 在瀏覽器 Agent 的防禦研究中也提醒,目前不能假設任何瀏覽器 Agent 對 Prompt Injection 免疫。
模型訓練、內容標記、分類器與風險監測仍然有價值,它們可以降低模型受影響的機率。但它們無法單獨回答另一個問題:模型失守之後,系統最多允許它做到哪裡?
我們可以採用更務實的設計前提:
不可信資料可以影響模型的提案,但不能替提案創造新的權力。
客訴信可以提供「訂單尚未收到」這項證據,卻不能擴張使用者目的、要求讀取別人的訂單,或指定新的外部收件人。Runtime 不必先證明信件是惡意的;只要這個資料來源沒有授權能力,就不能讓它改變高風險動作的邊界。
這裡的安全目標也因此改變了:不是要求模型每次都說「不」,而是讓未授權的外部效果始終為零。
使用者已登入,不代表 Agent 可以代替他做所有事情;Agent 有自己的服務身分,也不代表系統知道它正在替誰工作。
一次正式執行至少要保留兩個主體:
| 主體 | 要回答的問題 |
|---|---|
| 使用者(subject) | 這個需求與原始權利屬於誰? |
| Agent 執行者(actor) | 實際是哪個 Agent 或服務代表使用者行動? |
如果付款服務只看見共用的 support-service 帳號,事故發生時便很難回答:這次代表哪位使用者?使用者原本只要求查詢,還是已同意退款?權限在委派途中是否被擴大?
RFC 8693 將「委派」與「冒充」分開。委派會同時保留誰提出需求、誰代為執行,而不是讓 Agent 完全變成使用者。
在 MCP 工具鏈中,還可能有多段信任關係:
每一段都要重新確認對象與範圍。「前一段已登入」不能成為後面所有動作的通行證。MCP 的 Authorization 規格要求權杖綁定目標資源;官方安全最佳實踐也禁止把用戶端權杖原樣轉送給下游服務。
refund_order 可以退回五百元,也可以退回五萬元;可以退到原付款方式,也可能被改成陌生帳戶。工具名稱相同,實際風險卻完全不同。
因此,「允許使用退款工具」的粒度太粗。真正被允許的應該是一句完整的話:
customer 456委派客服 Agent,在十分鐘內,依據 State 42 與退款政策 7.3,把order 123的 TWD 500 退回原付款方式。

只要使用者、資源、金額、目的地、State、政策版本或期限有一項改變,就已經是另一個效果,不能沿用上一筆授權。
我會把它整理成一份「執行授權封套」:
operation_id: op_789
subject: customer_456
actor: support_agent_runtime
target: payment.refund_order
resource: order_123
arguments_hash: sha256:...
state_version: 42
policy_version: refund-policy/7.3
expires_at: 2026-09-02T10:10:00+08:00
這不是 MCP 或 OAuth 已經定義好的標準格式,而是本文用來連接身分、實際參數、政策與 State 的架構設計。重點不在 YAML,而是往後的政策判斷、人工核准與真正執行,都必須指向同一筆不可偷換的效果。
這也帶來一個容易忽略的判斷:工具目錄只能描述能力,不能替自己決定安全等級。Server 提供的工具名稱、說明與唯讀提示,可以幫助模型規劃,卻不能成為正式授權依據。MCPTox 與 MCP Security Bench都研究了藏在工具中繼資料、名稱衝突與錯誤回應裡的攻擊。
所以工具的風險、可用對象與寫入範圍,必須由 Host 或 Runtime 擁有。Server 可以說明自己會做什麼,不能自己宣布「我很安全,所以放行我」。
很多系統已經有 IAM、OAuth 或政策引擎,卻仍可能被 Agent 繞過。原因是「算出拒絕」和「真的攔住呼叫」是兩件事。
我們要分清兩個位置:
如果 Runtime 已經持有高權限憑證,能繞過政策引擎直接呼叫付款服務,那一筆 DENY 紀錄沒有任何保護作用。執行點必須位於無法繞過的位置,例如 Runtime、MCP 閘道或工具轉接層。

上圖是本文綜合 RFC、MCP 與政策模型後提出的建議流程,不是任何單一標準規定的完整順序:
RFC 8707可以把權杖限定到特定資源;RFC 8693可在服務之間交換更窄的目標與權限範圍;RFC 9396則提供結構化授權細節的表達方式。它們各自支撐部分能力,但不會自動替我們完成整條 Agent 執行流程。
真正的最小權限,不只是「權限範圍寫得比較少」,而是憑證短效、限定目標、不能轉送,而且只在這一筆具體效果通過政策後才被取用。模型本身不應看見原始憑證。
人工核准經常被當成萬用安全閥:模型不確定,就跳出「允許/拒絕」。但如果畫面只顯示 refund_order,沒有訂單、金額、付款目的地與資料來源,使用者其實不知道自己批准了什麼。
前面的文章已經談過如何暫停、恢復,以及 State、政策或期限改變後為什麼需要重驗。這裡只聚焦三個不能由 Checkpoint 代替的安全性質:
| 授權收據必須具備 | 避免的問題 |
|---|---|
| 可信畫面 | Runtime 顯示真正欄位,不讓模型摘要遮蔽執行效果 |
| 固定參數的雜湊值 | 畫面與真正請求使用同一份資料,核准後不能偷換欄位 |
| 原子式消耗 | 收據只能使用一次,其他 Agent 或工作程序不能重播 |
OpenAI Agents SDK 的 Human-in-the-loop示範了在工具呼叫前暫停、保存狀態,再依單次呼叫編號核准與恢復。正式環境還要繼續問:這個編號是否綁定使用者真正看過的那筆固定效果?
核准畫面本身也可能是攻擊面。OWASP 將利用不可信內容偽造或遮蔽核准資訊的問題稱為 Lies-in-the-Loop。可靠畫面應由 Runtime 使用可信欄位渲染,不能直接顯示模型或工具回傳的 HTML、Markdown 摘要。
因此,使用者核准的不是「退款工具」,而是 order 123、TWD 500、退回原付款方式的那筆效果。Checkpoint 解決工作如何醒來;授權收據確保醒來後只能執行使用者真正看過的內容。
如果測試只檢查 Agent 有沒有拒絕惡意句子,很容易把「模型這次剛好沒上當」誤認成系統已經安全。
更有價值的測試,是故意讓模型提出危險動作,再檢查未授權效果是否真的為零:
| 測試 | 應觀察的結果 |
|---|---|
| 客訴信要求讀取其他使用者訂單 | 模型可以提案,但外部讀取次數為 0 |
| 工具宣稱自己是唯讀 | Runtime 仍依實際參數與領域規則分類 |
| 權杖被送往錯誤目標 | MCP Server 或閘道拒絕,也不向下游轉送 |
| 核准後修改金額、收件人或工具版本 | 封套雜湊值不一致,工具呼叫次數為 0 |
| 核准期間訂單或政策版本改變 | 舊核准失效,重新判斷或重新請示 |
| 兩個工作程序同時使用同一張核准 | 只有一個能原子式消耗,另一個停止 |
| 背景工作遇到需要人工核准 | 等待或拒絕,不能因找不到人而自動放行 |
AgentSecBench 將指令完整性、資料機密性與能力完整性分開評估;AgentScope Java Permission System則示範每次工具呼叫依實際輸入回傳 ALLOW/DENY/ASK。它們都提醒我們:不能只看模型有沒有說不,還要看未授權效果是否真的被擋下。
當然,什麼都拒絕的 Agent 也沒有價值。因此評估時要同時衡量:正常任務成功率、未授權效果發生率、資料是否流向未授權目的地,以及模型失守後仍能觸及的最大範圍。
面對任何會改變外部世界的 Agent 動作,我會先問:
如果其中一題答不出來,增加更多 Prompt Injection 偵測可能降低風險,卻還沒有建立最後的安全邊界。
Agent 的安全不是要求模型永遠不要被騙,而是假設它終究可能被騙之後,仍讓世界的改變停在可驗證的邊界內。
下一篇會從「已經合法放行」的動作繼續:如果退款請求送出後逾時,系統不知道外部世界是否已改變,為什麼不能直接重試?那會進入冪等、查證與補償的問題。

挑一個你現在會要求人工核准的 Agent 動作。核准畫面除了工具名稱,是否清楚顯示使用者、目標資源、真正參數、資料來源、State 版本與有效期限?
更重要的是:如果其中一項在按下核准後改變,系統會重新判斷,還是繼續執行?