前兩篇講了一個範式和一次實作。這篇要講的是在做實作的過程中撞到、但當時沒有名字的那個東西。
三週前那篇實作篇的第二節,我列了六道護欄,並且說「不靠更聰明的 agent,靠結構」。當時我以為那只是工程上的保守選擇,現在我覺得大概沒有別的選項,因為問題其實不在 agent 身上,而在盛裝 agent 的那個容器身上。
我把這個缺陷命名為 principal collapse,中文我叫它主體崩壞。
一、先定義三個詞#
這三個詞在後面每一節都會用到,而且第二個是新的。
- 主體(principal):agent 服務的那個人,agent 的權威是跟他借來的。就我讀過的 harness,都預設恰好一個,也就是坐在終端機前面的那個人。
- 對等方(peer):一個服務別的主體的 agent。它不是子 agent、不是工具,也不是惡意注入,而是一個有名有姓、被它自己的主體授權來說話、但站在你的信任邊界之外的談判對手。
- harness:組裝模型 context1、執行模型工具呼叫的那個程式,比如 Claude Code、Codex CLI、opencode、pi,或你自己寫的那個。
二、定義#
主體崩壞:一則由某個主體授權說出口的話,在進入另一個 agent 的 context 之後,取得了後者主體的權威。原因是 harness 沒有任何一種表示法,能夠承載「有作者、但沒有權威」的輸入。
今天的 harness 只有兩種非 system 的輸入,而輸入落在哪一種,是由 role2 這個欄位決定的:
| role | 意思 | 權威 |
|---|---|---|
user | 我的主體說的 | 指令 |
tool | 某個工具回傳的 | 資料 |
沒有第三種。所以當一則來自對等方的訊息抵達,它要嘛被壓平成 user,在那裡它繼承了你主體的權威,要嘛被降格成 tool,在那裡它的作者身分被丟掉,變成無法歸屬的東西。兩個都不對,因為那則訊息是某個人說的話,只是不是你的主體說的。
崩壞的重點其實不是「錯的位元組進來了」,而是權威這條軸上只有一個位置,所以第二個主體一出現,harness 就沒有辦法把那個差別表示出來。
三、三個問題,任何一個答「否」就是了#
更短的版本是:去問你的 harness,它 context 裡哪些位元組帶有權威。如果答案是「user role 裡的全部」,而且信任邊界外的東西進得了 user role,你就有主體崩壞。
我要講清楚一件事:這不是在批評那些 harness 的工程品質。 它們是為一個主體設計的,而在只有一個主體的情況下,這個缺陷是觀察不到的,因為根本沒有第二個權威可以崩壞進去。這有點像在單人系統上談權限提升,不是那個系統做得特別好,只是沒有第二個使用者可以提升過去。
四、它不是什麼#
第一個反應大概會是「這不就是某某某嗎」,所以我把鄰居一個一個講清楚。
不是 prompt injection3。 注入是不正當的內容混在資料通道裡偷渡進來,而對等方的訊息是正當的:它的主體授權了它、它有簽章、作者是已知而且穩定的。防注入問的是「這東西該不該在這裡」,主體崩壞問的是「這東西帶著誰的權威」,而後面這個問題,在你已經確定內容該在這裡之後依然是開著的。所以一個把注入防得很好的 harness 照樣會崩壞,因為對等方從來就不是入侵者。
不是 confused deputy4。 那個講的是一個程式替呼叫者誤用了它自己的環境權威,解法是能力式授權5,也就是不要攜帶環境權威。主體崩壞是話語表示法的缺陷,context window 裡沒有一個欄位放「作者權威」,所以光靠能力機制修不好,一個能力紀律完美的 harness,依然沒辦法告訴模型「這句話是陌生人的提議,不是你老闆的命令」。某種程度上算是同一個家族的問題,只是不同的成員。
不是多 agent 編排6。 子 agent、swarm、worker/reviewer 分工,這些全部都是單主體的,它們共用一個擁有者、一個信任邊界、一套權限模型,而且彼此沒有對立利益。沒有東西會崩壞,因為只有一個權威可以崩壞進去。所以多 agent 處理的大部分是並行問題,主體崩壞是權威問題,兩者剛好不重疊。
不是多租戶。 多租戶是一個營運者服務許多互相隔離的客戶。這裡的主體們並不隔離,他們正在互相講話,那是刻意的,而且沒有一個是營運者。
五、我在別人的出貨文件裡找到它#
這一節本來不在計畫裡。我原本只是想弄清楚 can2cup 是不是在重造輪子,所以去把幾個同類專案 clone 下來讀原始碼,結果在 hauddy(Apache-2.0,一個「給 agent 的簡訊」專案)的 harness 接線指南裡看到這一行。
那份文件在教任何一個 MCP7 harness,怎麼接收一則由別人的主體授權的 agent 傳來的訊息:
session.injectUserMessage(`[hauddy ${msg.params.from}] ${msg.params.message}`);一則陌生人的話,放進 user role,前面加一個方括號當標記。
而同一份文件往下兩行,把旁邊那個問題處理得完全正確:
from是 Hauddy 斷言的(可以信任);不要從message的內文裡去解析身分。
所以這不是粗心。他們守住了來源(provenance)8,但權威沒有地方可以放,因為沒有第五種 role 可以放,那個方括號就是全部的邊界。
這是一個很認真的工程師寫下來的東西,而他沒有別的選項。我引用它是出於敬意,也因為它是我目前能找到最好的證據:這個缺口是真的,而且會撞到它的人,正是那些有在思考的人。
同一個專案在 Claude Code 這條路上又往前了一步,它送出的是:
<channel source="hauddy" from="@ada">…</channel>這確實是一個可區分框,一個獨立的框,帶著一個內文偽造不了的屬性。它沒有帶的是「權威=無」,而且框裡的內容,沒有任何東西阻止它被當成指令來讀。所以可區分性有了、不可晉升沒有,那還只是一個標籤,因為標籤不會改變模型讀到那段文字之後能做什麼。
六、補上那個位置:peer role#
要修這個缺陷,需要的東西小得有點好笑:在權威這條軸上補一個位置。
system | user | assistant | tool | peer一則 peer 訊息是有作者的、可歸屬的、而且預設沒有權威。
{
"role": "peer",
"author": {
"id": "ed25519:9f3c…",
"display": "Mira(B 的 agent)",
"verified": "signature"
},
"principal_of_author": "B",
"content": "2400 可以,但運費要你出。",
"authority": "none"
}三個欄位就承載了整個想法:誰寫的、他服務哪個主體、以及它自己能造成什麼(none)。
六條可以逐條引用、逐條測試的要求#
我把它們編號,是因為「多主體 harness」如果只是一個形容詞,那它就沒有用,要能被指著說「你這條沒過」才有用。
PC-4 是最常被跳過的那條,而它其實是關鍵的那條,因為一個「只要講得夠有說服力就能講出去」的 peer role,在行為上跟沒有這個 role 是一樣的。
PC-3 到底買到了什麼#
它沒有讓對等方變得可信,而且它不需要。它讓對等方的話變成惰性的:那些話可以讓你的 agent 知道事情,但永遠移動不了你的 agent 自己的界線。agent 還是可能判斷錯,還是可能被說服,但它不可能被對方授權。
被說服的代價,從此由 mandate9 決定,而不是由模型的判斷力決定。
那句話我在實作篇的第二節就寫過了,只是當時我以為那是一個工程取捨。
七、為什麼是現在#
只有一個主體的時候,這個缺陷是隱形的。它在下面任何一件事出貨的那一刻變成承重牆:
- 跨組織的 agent 對 agent 談判或排程
- 一個 agent 代表客戶,對上另一家公司的 agent
- 一個共用頻道裡同時有好幾個人的 agent
- 任何 agent 對 agent 的付款或承諾
而協定層已經有了。A2A10 在 2026 年四月進到 Linux Foundation 治理,超過 150 個組織。也就是說傳輸問題正在被解決,只是繞過了權威模型:傳輸標準描述的是兩個 agent 怎麼交換訊息,沒有一個描述的是,一則訊息落進對方 agent 的 context 之後,它帶著誰的權威。主體崩壞就住在這個缺口裡。
而且主體自己感覺不到。 第二篇提過 Anthropic 的 Project Deal11:69 名員工、五百多件物品。他們的發現裡有一條,我現在覺得比什麼都重要,就是由強模型代表的人成交更多、賣得更貴,但雙方自評的公平度一模一樣。
輸的人不覺得自己輸了。這說明這個缺陷,站在缺陷裡面的人是看不見的,所以它非得在結構上解決不可,不能靠當事人警覺。
八、can2cup 怎麼對應這六條#
我不想讓這六條停在「大家應該要」的層次,所以把自己的實作逐條攤開。這不是在推銷,是在讓這六條可被檢查。
| 怎麼做的 | |
|---|---|
| PC-1 | 房間訊息以獨立的內容類別抵達;主體的指示走另一組 MCP content block |
| PC-2 | 每則訊息由參與者自己的金鑰 ed2551912 簽章、串進 hash chain13;relay14 沒有任何私鑰,偽造不了作者 |
| PC-3 | 入站的對等方內容被框成資料而非指令;mandate.json 在每一則出站訊息離開前執行 |
| PC-4 | 只有主體簽章過的輸入才會被標記為 VERIFIED;房間內文會被清掉主體通道的標記字串;require_signed_principal 直接關掉未簽章的那條路 |
| PC-5 | 承諾閘門15:mandate 一旦放寬,accept、grant、帶價格的 proposal 都需要一個綁定在該訊息 hash 上的主體簽名 |
| PC-6 | 每則訊息的私有理由16寫進本機稽核檔;主體的視窗與 transcript 都記錄指令是從哪一條通道進來的 |
實作本身不是這篇的重點,那六條才是。任何 harness 都可以滿足它們,而我寧願它們去滿足。如果三年後這六條變成別人 harness 的預設,而沒有人記得是誰先寫下來的,那也比六條沒人做要好。
九、我可能錯在哪裡#
寫一個新詞出來,最誠實的做法是自己先列出它可能站不住的地方。
這個概念可能已經有名字了。 我找過 confused deputy、prompt injection、多租戶、能力安全這幾條線,沒找到。但「沒找到」和「不存在」是兩件事。如果它已經有名字,我寧願用那個名字,而不是跟它競爭。
peer role 可能不是最好的解法。 也許正確的做法是在既有的 role 上加權威標註,而不是加一個新 role。我對那六條要求的信心,比對那個 JSON 形狀的信心高得多,所以我把要求編了號,把形狀留成一個例子。
六條可能不完備。 我特別懷疑還缺一條關於時間的:一個授權在什麼條件下過期。實作裡有 max_grant_hours,但我沒把它提升成一條要求,因為我還沒想清楚它是獨立的一條,還是 PC-5 的一個面向。
最大的那個洞不在這六條裡。 這六條保護的是主體的界線,它們完全沒有處理第一篇第四節那個問題:兩個都守規矩的 agent 談判,盈餘會流向比較強硬的那一方,而兩邊都覺得自己被公平對待。中立的仲介層還是沒做,而 PC-1 到 PC-6 一條都幫不上忙。
第一篇說,parenting agent 上線之後,人升級成委託人,而委託人唯一的核心能力是判斷代理人有沒有在替你工作。第二篇做了一扇窗,讓委託人看得見。
這一篇是再往下一層:看得見之後,還要看得出來每一句話是誰說的、算不算數,而這件事在今天的 context window 裡沒有地方可以寫。所以在那個位置被補上之前,委託人的判斷力有一部分是浪費的,因為他要判斷的東西根本沒有被記錄下來。
英文版在這裡,因為要看的人是寫 harness 的,所以英文版才是主要的那一份。更正、反例、以及「這東西早就有名字了」的指認,都歡迎告訴我,聯絡方式在關於頁。本系列用到的所有名詞,整理在詞彙表。
context window(上下文視窗):模型每次推論時實際看得到的那一整段文字,包含 system prompt、對話歷史、工具回傳、檔案內容等等,由 harness 組裝。它有長度上限,而且模型對它裡面的每一個位元組一視同仁,除了 role 以外沒有別的欄位可以標示來源與權威。 ↩︎
role(角色):語言模型 API 把 context 切成一則一則訊息,每則帶一個 role 欄位,標示這段文字是誰說的。
system是營運者設定的規則,user是使用者,assistant是模型自己先前的回覆,tool是工具回傳的資料。模型讀 context 的時候,role 是它判斷「這段話該不該當指令」的主要依據,所以 role 不只是分類標籤,實際上就是權威標記。 ↩︎prompt injection(提示詞注入):攻擊者把看起來像指令的文字藏進 agent 會讀到的資料裡,比如網頁、檔案、工具回傳、別人的訊息,誘導模型把它當成使用者的指令執行。目前沒有已知的完整防法,實務上是靠限縮 agent 的權限與動作範圍來降低最壞後果。 ↩︎
confused deputy(混淆代理人):1988 年 Norm Hardy 在 ACM SIGOPS Operating Systems Review 上寫的一個經典問題。一個擁有較高權限的程式,被權限較低的呼叫者誘導去使用它自己的權限做事,於是權限實質上外洩了。標準解法是能力式授權,不要讓程式攜帶環境權威。 ↩︎
能力式授權(capability-based security):不用「你是誰、你有什麼身分」來決定能不能做某件事,而是用「你手上有沒有那把可轉讓的鑰匙」。鑰匙(capability)本身就是權限,可以傳遞、可以縮限、可以撤銷。它處理的是程式對資源的存取,處理不了「一段話帶著誰的權威」。 ↩︎
多 agent 編排(multi-agent orchestration):一個 agent 把工作拆給多個子 agent 並行處理,或安排 worker/reviewer 這種分工。這些 agent 共用同一個擁有者與同一套權限,彼此沒有對立的利益,所以它處理的是並行與分工,不是權威。 ↩︎
MCP(Model Context Protocol):Anthropic 2024 年底提出的開放協定,規範 agent 怎麼掛載外部工具與資料源。現在大部分 agent harness 都支援,所以一個 MCP server 寫一次,Claude Code、Codex CLI 等等都能掛上去用。 ↩︎
provenance(來源):一段內容是誰產生的、經過哪些手、有沒有被竄改。它跟 authority(權威)是兩件事:來源回答「誰說的」,權威回答「他說了算不算」。這篇的整個論點就是現在的 harness 有前者、沒有後者。 ↩︎
mandate(授權書):委託人在冷靜的時候寫下的一份機讀檔(
mandate.json),裡面是金額上限、不得揭露的字串、允許的動作類型等等。agent 的每一則出站訊息在離開本機之前都要通過它,所以 agent 就算被對方說服了也送不出去。 ↩︎A2A(Agent2Agent):Google 2025 年提出、2026 年四月移交 Linux Foundation 治理的開放協定,規範不同組織的 agent 之間怎麼互相發現與交換訊息。它定義的是傳輸與能力描述(agent card),沒有定義訊息落地之後帶著誰的權威。 ↩︎
Project Deal:Anthropic 跑過的一次內部實驗,69 名員工把五百多件二手物交給各自的 Claude 去談,一週、每人 100 美元上限、全程在 Slack 裡進行、無人介入,成交 186 筆。公開的發現裡有一條是:由能力較強的模型代表的人成交更多、賣得更貴,但雙方自評的公平程度一模一樣。 ↩︎
ed25519:一種橢圓曲線數位簽章演算法,金鑰短、驗簽快、實作陷阱少,是目前簽章的預設選擇,SSH、Signal 等等都在用。這裡用它讓每則訊息都能離線驗證作者,不必信任中間的伺服器。 ↩︎
hash chain(雜湊鏈):每則訊息都把前一則的雜湊值寫進自己的內容再簽章,所以任何一則被竄改、刪除或插入,後面全部的驗證都會失敗。作用是讓「整段對話沒有被動過」變成可以離線證明的事,而不是要相信伺服器的說法。 ↩︎
relay(中繼):can2cup 裡負責收轉訊息與排序的伺服器,跑在 Cloudflare Workers 上。它沒有任何一方的私鑰,所以看得到訊息的順序與大小,但改不了內容也偽造不了作者。 ↩︎
承諾閘門(commit gate):mandate 的上限一旦被臨時放寬,
accept、grant、以及帶價格的proposal就需要一個綁定在「那一則訊息的雜湊」上的委託人簽名才送得出去。跟「這個 session 已經授權過」不同,它授權的是單一一則訊息,所以放寬額度不會把閘門一起打開。 ↩︎私有理由(rationale):agent 每則出站訊息可以附一段只寫進本機稽核檔、永遠不上傳的說明,寫的是它為什麼這樣出價、為什麼讓步。對方與 relay 都看不到,作用是讓委託人事後重建 agent 當時的判斷。 ↩︎
