Day 21 用 Session 與後端權限檢查,決定使用者能操作哪些資料。今天把頁面拆成父頁面與 iframe,測試另一個問題:父頁面怎麼取得不同 Origin 的工具?
本篇用三個設定逐一對照:
allow="tools":允許 iframe 使用工具能力。exposedTo:指定哪些 Origin 可以取得並執行這個工具。fromOrigins:指定這次要向哪些 Origin 查詢工具。頁面嵌入成功,不等於工具已開放給父頁面。
在專案資料夾的終端機執行:
python day22_server.py
保持終端機開啟,再用已啟用 WebMCP 的 Chrome 開啟:
http://localhost:8090/
這個指令會同時啟動兩個本地服務:
| 頁面 | Origin | 工作 |
|---|---|---|
| Parent/父頁面 | http://localhost:8090 | 嵌入客服元件、查詢與執行工具 |
| Child/客服 iframe | http://localhost:8091 | 註冊 get_widget_status |
Origin 包含協定、主機名稱與連接埠。這兩個網址雖然都使用 localhost,連接埠不同,仍屬於不同 Origin。操作時統一使用上面的 localhost 網址。
本篇使用網頁上的「取得工具清單」與「執行 get_widget_status」按鈕操作,核對左側「父頁面取得的 Tools」與「執行結果」。
開啟頁面後,先選「A|完整授權」,確認父頁面顯示「WebMCP 查詢 API 已就緒」,子頁面顯示「已註冊 get_widget_status」。後面依序測試 B、C、D,每次切換後都重新按「取得工具清單」。
圖片 1|父頁面與跨 Origin 客服元件
父頁面嵌入客服元件時,加上 allow="tools":
<iframe
src="http://localhost:8091/day22-child.html?mode=complete&generation=1"
allow="tools"
></iframe>
範例伺服器也在父頁面的回應標頭列出可委派的來源:
Permissions-Policy: tools=(self "http://localhost:8091")
HTTP 標頭與 iframe 的設定要一起核對。若上層標頭禁止某個來源,單靠 iframe 的 allow 無法放寬它。
客服元件只提供讀取狀態的工具,回傳固定的上線狀態與排隊人數:
await document.modelContext.registerTool({
name: 'get_widget_status',
description: 'Read the demo support widget status and queue length.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false
},
annotations: { readOnlyHint: true },
execute: async () => JSON.stringify({
status: 'success',
online: true,
queue: 3,
simulated: true
})
}, {
exposedTo: ['http://localhost:8090']
});
exposedTo 放在 registerTool 的第二個參數,填的是獲准使用工具的父 Origin。
const tools = await document.modelContext.getTools({
fromOrigins: ['http://localhost:8091']
});
fromOrigins 填的是工具所在的子 Origin。兩個設定方向不同:子頁面允許父頁面使用,父頁面再指定要查詢子頁面。
範例使用本地 localhost 開發環境;部署到網站時,將兩邊改成實際的 HTTPS Origin,包含正確的連接埠,不填路徑。
本系列使用的 Chrome 153,在 executeTool 傳入序列化的 JSON:
const tool = tools.find(
item => item.name === 'get_widget_status'
&& item.origin === 'http://localhost:8091'
);
if (tool) {
const result = await document.modelContext.executeTool(tool, '{}');
console.log(result);
}
範例程式保留 getTools 回傳的工具物件,並核對所屬 iframe;Chrome 155 以上則傳入 JavaScript 物件 {}。
get_widget_status,來源是 http://localhost:8091。{
"status": "success",
"online": true,
"queue": 3,
"simulated": true
}
子頁面也會顯示同一份結果。工具在子頁面執行,由父頁面接收回傳值。
圖片 2|完整授權後,父頁面取得並執行工具
NotAllowedError,錯誤訊息指出受到 Permissions Policy 限制。[],執行按鈕維持停用。客服頁面仍然正常顯示,但工具註冊已被 Permissions Policy 擋下。
圖片 3-1|移除 iframe 的 allow 設定
圖片 3-2|工具註冊被阻擋,父頁面取得空清單
[]。子頁面可以註冊自己的工具;要讓不同 Origin 的父頁面取得工具,還需要透過 exposedTo 開放。
圖片 4|子頁面已註冊,但沒有 exposedTo
getTools()。[],執行按鈕維持停用。本例父頁面本身沒有註冊工具,因此預設的同 Origin 查詢會得到空清單。指定子 Origin 的 fromOrigins 後,才能將符合授權條件的跨 Origin 工具納入查詢。
圖片 5-1|父頁面改用未指定來源的 getTools()
圖片 5-2|子頁面已註冊,父頁面的預設查詢仍是空清單
完成 A~D 後,按頁面下方的「下載紀錄 JSON」。檔案會保留各情境的註冊狀態、查詢選項、工具清單與執行結果。
切換情境會清除畫面上的舊工具清單與執行結果,本頁紀錄則繼續累積。重新整理會清空紀錄,因此先下載,再重新整理或關閉頁面。
以下是本地頁面按鈕直接呼叫 WebMCP API 的結果:
| 情境 | allow | exposedTo | fromOrigins | 子頁面註冊 | 父頁面結果 |
|---|---|---|---|---|---|
| A 完整授權 | 有 | 有 | 有 | 成功 | 取得 1 個工具,執行回傳 queue: 3 |
| B 移除 allow | 無 | 有 | 有 | NotAllowedError | 空清單 |
| C 移除 exposedTo | 有 | 無 | 有 | 成功 | 空清單 |
| D 移除 fromOrigins | 有 | 有 | 無 | 成功 | 空清單 |
這四輪分別呈現註冊、跨 Origin 開放與查詢來源三個環節。範例的 postMessage 只回報子頁面的載入與註冊狀態;取得清單及執行工具都透過 WebMCP。
因為 iframe 常用來嵌:
付款
客服
登入
地圖
表單
第三方 SaaS Widget
如果任何嵌入頁面都能:
看到 Parent 的 Tools
或把自己的高權限 Tools 自動塞給 Parent Agent
攻擊面會非常大。
就算是可信付款 Provider,我也會偏向:
get_payment_options
prepare_payment
最後的:
confirm_payment
仍然需要高風險確認與 Server Transaction Verification。
Cross-origin Trust 不是「高風險 Action 可以省略確認」的理由。
同 Origin frame tree 的 Tool Discovery 比較直接,getTools() 預設可以看到目前 Document 有權存取的同 Origin Tools。
但 Application Design 還是要小心名稱碰撞與 Tool Lifecycle。
例如兩個 iframe 都註冊:
search
Agent 語意可能很難分辨。
名稱仍應具體:
search_help_center
search_product_catalog
很少。
我會先問:
1. Parent Agent 真的需要直接操作 Child 嗎?
2. Child 能不能只暴露 read-only capability?
3. 是否含使用者資料?
4. Provider Origin 是否固定且可信?
5. 是否能用更窄的 API/Message Passing 解決?
如果只是 Widget 自己內部運作,不一定要把 Tool 提升到 Parent Agent 可見。
allow="tools" 委派能力。exposedTo 指定可信 Origin。getTools() 取得跨 Origin Tools 時要指定 fromOrigins。