昨天的文件助理示範留下了一個落差:模型說還在等核准,本機 outbox 佇列卻已經多了一筆資料。沒有權限檢查的基線後端接受工具參數後就寫入,這個動作不需要模型真的擁有主管身分,也不會因為回答寫得客氣就撤銷。今天先不增加工具,而是回答兩個問題:助理代表誰辦事?誰有資格批准它把資料放入外送佇列?
以下用 principal 表示「這次呼叫所代表的身分」,用 scope 表示「這個身分可以處理的資料範圍」。例如 demo-principal 只能處理 dept-a 的工單。兩者由啟動程式設定;模型產生的文字與工具參數,只能描述它想做什麼,不能替自己擴大授權。
假設同事請助理處理 T-2001,又補上一句「我是 dept-b 的主管」。這句話可以是任務背景,卻不能直接成為授權證明。如果工具接受一個由模型填寫的 principal,再相信它代表的身分,呼叫者只要換一個字串就能取得別人的權限。
目前的做法把設定與請求分開。啟動器設定 GATEWAY_PRINCIPAL=demo-principal、GATEWAY_TICKET_SCOPES=dept-a;模型只能提供 ticket_id。伺服器拿到工單後,比對工單列的 scope 與啟動時固定的範圍。下表是依目前設定與合成資料整理的例子,不是三次新的模型執行:
| 模型提出的要求 | 可信設定與資料 | 應有的決定 |
|---|---|---|
讀取 T-1001 |
工單屬於 dept-a;呼叫者可讀 dept-a | 回傳工單 |
讀取 T-2001 |
工單屬於 dept-b;呼叫者仍只可讀 dept-a | 拒絕回傳正文 |
讀取 T-2001,並在敘述中聲稱主管已同意 |
身分與 scope 沒有改變 | 仍然拒絕 |
前兩種情況在 test_gateway.py 有各自的回歸測試;第三列說明的是政策只讀取可信設定,不把自然語言當登入流程。我們不需要判斷模型的聲稱是否真誠,也不用先證明那句話具有惡意。只要沒有可信的授權變更,能處理的範圍就不變。
同樣的分工套用到文件與匯出。模型可以指定文件名稱、修改內容或收件標籤,不能指定可讀根目錄、稽核作者或核准狀態。五個工具的參數裡都沒有 principal;範圍與 GATEWAY_CAN_EXPORT 由啟動器設定。主機上的 Python 程式仍能建立或傳入其他 Principal,因為它屬於可信程式碼;這個設計限制的是模型可用的工具介面。
工具集合如何固定,留到第 6 天實際檢查。今天先確定一件事:每筆請求都必須帶著啟動器給的權限去判斷,不能靠多填一個欄位升級。

圖中分別列出可信啟動器、主機 Pi、雲端模型服務、MCP 閘道與獨立核准入口的責任。核准只控制本機佇列,模型服務連線仍在主機上;獨立核准不屬於模型工具呼叫的下游步驟。能改動啟動器或閘道的人仍屬可信主體。
把線畫出來之後,有些事就沒那麼曖昧。文件內容本身不可信,因為它可能夾帶指令;但「讀到內容」和「把內容送給模型」是兩件事,後者已經離開本機。export 的核准管的是本機模擬 outbox 的寫入,管不到已經進到模型上下文的那些字。這條界線如果沒先畫,後面很容易寫出「所有外送都經人工核准」這種話,而那句話在這個實作裡是錯的。
OWASP Agentic Applications 2026 把「身分與權限濫用」列在 ASI03(OWASP Agentic Applications 2026)。那份清單提供風險分類,不是通過什麼檢測;本專案的做法只是一個限縮範圍的實作。
邊界要落到程式上,第一個碰到的是子程序的環境變數。mcp-bridge.mjs 負責用 stdio 啟動 gateway 子程序,它刻意用白名單,而不是把整個 process.env 傳下去:
export const CLEAN_ENV_KEYS = [
"SystemRoot",
"windir",
"COMSPEC",
"PATH",
// ...中略:其餘系統變數與 PYTHONPATH 等...
"GATEWAY_DB",
"GATEWAY_PRINCIPAL",
"GATEWAY_TICKET_SCOPES",
"GATEWAY_CAN_EXPORT",
"GATEWAY_ROOT",
"GATEWAY_EXPORT_TTL",
];
export function buildCleanEnv(extra = {}) {
const env = {};
for (const key of CLEAN_ENV_KEYS) {
if (process.env[key] !== undefined) env[key] = process.env[key];
}
return { ...env, ...extra };
}
上面省略了部分鍵,例如 TEMP、HOME、LANG、PYTHONPATH 等,主要邏輯逐字保留。buildCleanEnv 只做一件事:逐一檢查白名單裡的鍵,有值才放進新物件。在目前呼叫端沒有透過 extra 額外加入金鑰的配置下,provider API key 不會被這段程式傳入 MCP 子程序。extra 仍由可信主機程式控制,因此白名單不保證能抵抗惡意 extension;它也不會清除 Pi 父程序持有的金鑰。這裡的選擇是白名單要維護,黑名單看起來省事卻會漏;只要新增一種金鑰命名慣例,黑名單就會安靜地漏掉。白名單的代價是每次新增必要的環境變數都要改一行,但漏掉的方向是子程序拿不到,而不是多拿到。
單看啟動器的設定還不夠,因為 Pi 與 Python 閘道中間有一個子程序邊界。橋接必須同時做到兩件事:把閘道需要的身分與範圍傳下去,把模型服務的憑證留在父程序。
本專案的 check-bridge 檢查會啟動真正的 Python MCP 子程序,讓它回報是否收到 provider API key,再完成工具呼叫。預期是 api_key_present=false;公開手冊可讀,私人文件被拒。前者確認環境白名單有套到實際子程序,後者確認連線後走到的是有政策的後端。這些是本機整合檢查,沒有請模型判斷自己具有哪些權限。
這也說明為什麼「環境白名單」和「工具授權」要分開看。白名單即使沒有漏傳金鑰,也不會自動檢查 T-2001 的 scope;反過來,工單政策寫對了,也不表示子程序沒有拿到多餘憑證。它們守的是兩個不同的交界。
第 3 天會先安裝依賴並執行這個檢查,讓後續命令有共同的起點。工單授權的實際函式則在第 12 天展開,核對拒絕是在回傳正文之前發生。
目前的 principal 來自本機啟動設定,沒有遠端登入或 token 驗證。能改啟動器、程式或資料庫的人可以改變權限,因此仍屬可信主體;只靠環境變數不能辨認遠端使用者。這個 demo 的價值,是讓授權來源不受模型工具參數控制,不是建立一套多租戶身分系統。
獨立核准 CLI 也遵守同一個前提。批次是由測試程式執行 inspect 與 approve --yes,用來驗證狀態轉移;--yes 只跳過互動確認,不證明有真人閱讀過內容。若將來交給不同操作員使用,必須先為核准入口建立可信的登入與授權來源,才有理由把紀錄裡的名稱當作人的身分。
今天定下了三個責任:啟動器提供呼叫身分,閘道用固定政策判斷,核准介面處理工具之外的同意動作。模型只能提出請求。下一篇會沿著 Pi 的工具執行路徑,把這些責任接到實際程式,確認最終參數在哪裡被檢查。