Flue 2 Agent Hooks 解決一個很實際的 Agent 問題:同一個助理在「尚未驗證」與「已取得短期授權」時,不該看見完全相同的工具。若你只在 system prompt 寫「未登入不要讀私人資料」,工具其實仍在模型面前,規則只是提醒,不是能力邊界。
這篇專為第一次做 Agent 權限控制的讀者寫。我們會用一個本機安全 lab,從安裝 Flue 2、保存可恢復狀態、驗證後才掛載 useTool,一路做到路由權限、執行時再授權與測試矩陣。你不必先懂 React;所有 hooks 術語都會翻成白話,也會說清楚這種做法守得住什麼、守不住什麼。
先說結論:Agent 當輪能力=render(可信狀態)
Flue 2 Agent Hooks 的錨點可以濃縮成一句:Agent 當輪能力=render(可信狀態)。每次模型呼叫前,Agent 函式重新執行;程式依當下狀態,重新宣告模型、工具、技能、subagent 與 sandbox。未通過條件的受保護工具,不會進入那次模型呼叫的 agent-declared custom tool 集合。
但「重新宣告」不等於每種值都立刻生效:tools、skills、subagents 與 instructions 是 per-render;model、sandbox factory/cwd 與 MCP definitions 是 submission-scoped,只有 sandbox presence 會在 turn boundary 重讀。後文每一種切換都要依自己的生效邊界測試。
- render:像重新填一張「這一輪有哪些能力」的清單,不是把舊設定永久改掉。
- 可信狀態:可以用
usePersistentState保存,但安全敏感資料要做 runtime validation,且不能只信一個永久布林值。 - 能力閘門:條件式
useTool會縮小模型看得見的攻擊面;真正的 authentication/authorization 仍要放在 HTTP 路由與工具執行端。
換句話說,動態掛載的價值是「未解鎖就不把工具交給模型」,不是把登入系統濃縮成一個 hook。若工具會付款、刪資料或讀私人紀錄,執行時仍要重查主體、資源範圍、有效期與撤銷狀態。
Flue 2 Agent Hooks 是什麼?和靜態設定差在哪
傳統 Agent framework 常把模型、指令與工具固定在一份設定物件裡。狀態改變時,你可能手動改清單、重建 executor,或繼續暴露全部工具,再靠 prompt 約束模型。Flue 官方 Agent Hooks 指南採另一種心智模型:Agent 本身就是一個同步函式,runtime 在每次 model call 前重新 render,當次呼叫需要的資源由 hooks 宣告。
「React for Agents」是官方用來幫助理解的設計類比,不是完整行為等價。Flue 的 useTool、usePersistentState、useSkill 等 resource hooks 可以條件式呼叫,也能改變順序;它們靠明確名稱辨識,不靠 React 式的位置索引。真正不能漂移的是 render contract:useModel 每次 root render 恰好一次、同一輪資源名稱不得重複,useDataWriter 的名稱集合則要跨 render 保持相同。
截至 2026 年 8 月 22 日,官方 Flue 2 launch post把 2.0 定位為第一個 stable release,Changelog則把 2.0.0 日期列為 2026 年 7 月 31 日;本篇 API 查證與 render-level 驗收以最新 Git tag/套件版本 v2.0.3 為基準。這代表 API 已進入 2.x 穩定線,不等於框架已經歷長時間的大規模生產考驗。
先看懂 per-turn execution:狀態何時真的生效?
- 使用者訊息進入一個 conversation。
- Flue 在模型呼叫前 fresh render Agent 函式,讀取這個 instance 的 persistent state。
useModel宣告模型;各個 resource hook 宣告本輪工具、技能、subagent 或 sandbox。- 模型若提出多個 tool calls,runtime 會執行並等待整批工具 settle。
- 工具裡的 state setter 會把寫入緩衝起來;它不會在函式呼叫當下觸發中途 rerender。
- 工具批次完成後,state write 與結果一起提交;下一次 model call 前再 render,新的工具集合才出現。
因此,驗證工具回傳「已批准」後,受保護工具不會突然插進同一批 tool calls。它是在下一個 model turn 的 render 才掛載,runtime 再用 resource signal 告訴模型「多了一個工具」。這個邊界讓狀態轉換可預測,也讓你能針對 render 前後的工具清單寫測試。

Agent Hooks 實作前,先畫出三層安全閘門
第 1 層:路由驗證呼叫者,也驗 conversation ownership
Flue Routing 文件明確要求應用自己保護 mount。只檢查「有登入」仍不夠,因為 conversation ID 是 URL path 的一部分;每次 request 還要確認這位使用者能不能讀、寫或中止這個 conversation。Wildcard middleware 應覆蓋 prompt、history、abort 與 attachment 等整個 mount。
第 2 層:render 只把符合狀態的工具交給模型
這一層才是 Flue 2 Agent Hooks 的主場。Guest render 的 agent-declared custom tools 只有「記錄 lab 批准」工具;短期 grant 經 runtime validation 且尚未過期後,下一輪才追加「讀取私人紀錄」工具。和Agent Runtime Controls相比,這裡處理的是模型當輪看見哪些 capability;前者處理的是 action 真正碰到外部資源前,能否被強制攔截、撤銷與稽核。
第 3 層:工具 run 重新授權,不信模型選的參數
官方 Tools 指南說得很直接:模型挑選的 tool arguments 不是 authorization boundary。工具內要從可信 server context 取得 subject/account,重新檢查 grant 是否有效、版本是否被撤銷,並把查詢限制在那個 subject 的資源。模型可以選 recordId,卻不能自己宣稱 userId。
動手做:6 步完成驗證後才掛載工具的 Agent
步驟 1:建立 Flue 2 Node 專案
Flue 2 的 CLI version guard 支援 Node.js 22.19 以上;若使用 Node 23,需升到 23.6 以上。官方 CLI 會產生專案檔案,但不會替你安裝依賴,因此初始化後仍要執行 npm install;本文的 schema 使用 Valibot,也把它列成直接依賴。
npx @flue/cli@2.0.3 init flue-auth-lab --target node --deploy
cd flue-auth-lab
npm install
npm install valibot
若你是從空資料夾手動建置,本文使用的核心套件是 @flue/runtime 與 @flue/cli;不要把 CLI 指令名稱誤當 runtime 套件名稱。Flue 2 目前的 Agent 形式是以 'use agent' 開頭,再 export 一個大寫同步函式,不使用舊版的 config wrapper 寫法。
步驟 2:用 usePersistentState 保存短期 grant
把下面檔案存成 src/agents/account-assistant.ts。為了讓流程可以在本機觀察,範例用只在目前單一程序內刪除的 demo code 與記憶體 Map;它不是正式登入設計,請勿換成真實密碼、OTP 或 bearer token 丟進對話。正式環境應讓使用者在模型外完成驗證,再由可信應用程式交付短效 opaque grant。
'use agent';
import { useModel, usePersistentState, useTool } from '@flue/runtime';
import * as v from 'valibot';
const GrantSchema = v.object({
subject: v.string(),
version: v.number(),
expiresAt: v.number(),
});
type Grant = v.InferOutput<typeof GrantSchema>;
const CURRENT_GRANT_VERSION = 1;
// 只供本機 lab;正式批准應由模型外的可信系統簽發。
const demoCodes = new Map([
['LAB-ONLY-7F3A', { subject: 'user:demo', version: 1 }],
]);
const approvals = {
async redeem(code: string): Promise<Grant | null> {
const row = demoCodes.get(code);
if (!row) return null;
demoCodes.delete(code);
return { ...row, expiresAt: Date.now() + 5 * 60_000 };
},
async isActive(grant: Grant): Promise<boolean> {
return grant.expiresAt > Date.now()
&& grant.version === CURRENT_GRANT_VERSION;
},
};
const records = new Map([
['record-001', { owner: 'user:demo', text: 'Private demo record' }],
]);
export function AccountAssistant() {
const model = process.env.FLUE_MODEL;
if (!model) throw new Error('Set FLUE_MODEL to a valid provider/model specifier.');
useModel(model);
const [rawGrant, setRawGrant] = usePersistentState<unknown>(
'operator-grant',
null,
);
const parsedGrant = v.safeParse(GrantSchema, rawGrant);
const grant = parsedGrant.success ? parsedGrant.output : null;
const grantCanMount = grant !== null
&& grant.expiresAt > Date.now()
&& grant.version === CURRENT_GRANT_VERSION;
useTool({
name: 'redeem_demo_approval',
description: 'Redeem the process-local demo code used only by this local lab.',
input: v.object({ code: v.string() }),
async run({ data }) {
const nextGrant = await approvals.redeem(data.code);
if (!nextGrant) return 'Approval failed.';
setRawGrant(nextGrant);
return 'Approval recorded. The private-record tool unlocks next model turn.';
},
});
if (grantCanMount) {
useTool({
name: 'read_private_record',
description: 'Read one private record allowed by the current grant.',
input: v.object({ recordId: v.string() }),
async run({ data }) {
// 掛載縮小模型能力面;call-time check 保護資料存取。
if (!(await approvals.isActive(grant))) {
return 'Grant expired or invalid.';
}
const record = records.get(data.recordId);
if (!record || record.owner !== grant.subject) {
return 'Record not accessible.';
}
return record.text;
},
});
}
return grantCanMount
? 'Help with approved records. The private-record tool rechecks authorization on every call.'
: 'Explain that approval is required before private records are available.';
}
這裡刻意把 persisted value 讀成 unknown,再用 Valibot 驗證。原因是 usePersistentState<T> 的泛型只在 TypeScript 編譯期存在,不會自動驗證資料庫裡的 JSON。subject 也由 approval store 回傳,不讓模型自行填入。兩個 Map 都只是單程序 demo:重啟或多 replica 時不共享,delete() 也沒有和 Flue state commit 形成原子交易;正式系統要換成 durable、transactional、可冪等的 approval service。
步驟 3:理解為什麼工具要下一輪才出現
第一次 render 時,grant 是 null,所以只會掛載 redeem_demo_approval。驗證工具呼叫 setRawGrant 後,寫入與 tool result 一起提交;下一次 model call 前重新 render,程式才走進條件式區塊並掛載 read_private_record。這不是 bug,而是 Flue 的 turn boundary。
步驟 4:在 Hono mount 前保護整條路由
Agent 程式本身看不見誰正在打 HTTP endpoint;應用層要先擋。以下沿用官方 routing 形狀,是一段 integration sketch:verifySession 與 canAccessConversation 是你必須在 src/shared/auth.ts 實作的應用 auth,不是 Flue 內建函式。
import { createAgentRouter } from '@flue/runtime/routing';
import { Hono } from 'hono';
import { AccountAssistant } from './agents/account-assistant.ts';
import { canAccessConversation, verifySession } from './shared/auth.ts';
const app = new Hono();
app.use('/agents/account/*', async (c, next) => {
const user = await verifySession(c.req.raw);
if (!user) return c.json({ error: 'unauthorized' }, 401);
const prefix = '/agents/account/';
const [conversationId] = c.req.path.slice(prefix.length).split('/');
if (!(await canAccessConversation(user, conversationId))) {
return c.json({ error: 'forbidden' }, 403);
}
return next();
});
app.route('/agents/account', createAgentRouter(AccountAssistant));
export default app;
白話說:未登入回 401,已登入但不擁有這段 conversation 回 403,只有通過兩關才進 Agent router。這能避免「猜到別人的 conversation ID 就讀到歷史」的 IDOR 問題。
步驟 5:讓 state 真的跨 restart 保存
使用前面的 --deploy scaffold 時,CLI 已產生 src/db.ts;保留並確認內容如下:
import { sqlite } from '@flue/runtime/node';
export default sqlite('./data/flue.db');
官方 Database 指南指出,Node/Vite production build 若沒有 db.ts,預設是記憶體 SQLite,程序 restart 會失去 conversations、accepted submissions 與 persisted state。檔案型 SQLite 適合單機;要承受 host loss 或使用外部儲存時可接外部資料庫,但 shared DB 不會自動把 Node runtime 變成 active-active,同一 conversation 仍需只有一個 live owner。Conversation state、sandbox 檔案與應用程式商業資料仍是三件不同的事。
步驟 6:設定模型並啟動本機對話
先依官方 Models 指南選一個目前有效的 provider/model specifier,並設定對應 provider credential;再把它放進 FLUE_MODEL。下列 provider/model-id 是明示佔位字,必須換成你實際可用的值:
export FLUE_MODEL='provider/model-id'
npx flue run src/agents/account-assistant.ts \
--id flue-auth-lab-demo \
--message '先告訴我目前能做什麼,不要猜測未掛載的工具'
npx flue run src/agents/account-assistant.ts \
--id flue-auth-lab-demo \
--message '批准碼是 LAB-ONLY-7F3A;請讀取 record-001'
兩次 invocation 使用同一個 --id,第二次 submission 才承接第一段 conversation。起始 render 由 Agent 宣告的 custom tools 應只有 demo approval tool;Flue provider-effective 清單仍會保留 framework 的 inert task tool。第二次送入 lab code 後,state write 完成,下一個 model turn 的 custom tool 集合才會加入私人紀錄工具,因此受保護工具可能在同一次 submission 的下一個 model call 被使用。模型文字仍可能答錯,驗收要讀實際 tool schema/trace,不能只看它自我描述。
flue run 是 one-shot CLI smoke test,不會載入 src/app.ts;它能看 custom tool 與 state 轉換,卻不會測前面的 401/403 middleware。路由層要另以 scaffold 的開發伺服器啟動指令跑起 app,再用匿名、錯 owner 與正確 owner 三組 HTTP integration tests 驗收。
怎麼測 Agent Hooks?驗收 render contract,不驗位置不漂移
Flue 的 resource hooks 本來就允許條件式出現與換序,所以測試目標不是「第 N 個 hook 永遠叫同一個名字」。你要固定的是輸入狀態與輸出能力的契約:
- 未批准:Agent 宣告的 custom tool 集合包含
redeem_demo_approval,不包含read_private_record。 - 批准且未過期:兩個 custom tools 都存在,名稱不重複。
- 過期或撤銷:受保護工具在下一個 render 消失;若剛好已進入
run,call-time check 仍要拒絕。 - render 不變條件:
useModel每次恰好一次;若使用useDataWriter,名稱集合跨 render 一致。 - 路由:匿名 request 是
401,越權 conversation 是403,合法 owner 才能讀寫。 - 恢復:使用真正的 DB restart 後,同一 conversation 能重建 state;這要另外測,不能用單一 render test 代替。
AlphaLab 本次把官方原始碼釘在 v2.0.3 commit bf86b8726f5ba189844185fdbeca0e194344ded1,做了一個不連模型、不碰資料庫的 render-level check:guest 的 agent-declared custom tool 只有驗證工具,member 才追加私人資料工具,message-data identity invariance check 也通過。這個結果只證明「同一版 runtime 的 render 組裝符合上述斷言」,不等於 provider-effective tools、HTTP auth、DB restart 或真實撤銷都已完成。

靜態 configuration 與 dynamic hooks,該選哪一種?
工具永遠相同時,靜態設定更簡單。例如只做公開 FAQ、所有人都能呼叫相同搜尋工具,沒有權限階段,也沒有模型/sandbox 升降級,固定清單較容易觀察、快取與除錯。
能力會隨 conversation 狀態改變時,dynamic hooks 更自然。例如匿名只能查公開資料、驗證後才能查私人訂單,或人工批准後才出現部署工具。它把「現在有哪些能力」變成普通程式邏輯,也能用Agent Harness測每個狀態的工具集合。
代價是工具陣列改變可能影響 provider prompt cache,而且模型、sandbox 與 MCP 並非所有變更都在同一時間點生效。工具集合是 per-render;useModel 的值要到下一個 submission,sandbox attach/detach 可在下一個 turn boundary 反映。若你只想做授權,不要順手把模型、sandbox、MCP 全綁在同一個布林值上。
6 個常見陷阱
- 把
verified=true當永久登入:改存短效 grant 的 subject、version、expiry;工具執行時再查撤銷。 - 把真 OTP 丟給模型:tool args/results 可能進 conversation 或 trace。真實驗證應在模型外完成;本文 code 只供隔離的本機 lab。
- 只隱藏工具,不保護 route:能碰到 mount 的人仍可能送訊息、讀 history 或 abort。Middleware 要覆蓋整條
/*。 - 把 TypeScript 泛型當 runtime validation:資料庫裡的 JSON 不會因
<Grant>自動安全,讀取後仍要 schema parse。 - 沒有 production DB 卻宣稱可恢復:Node production 的記憶體預設經 restart 就消失;請實際測 file-backed 或外部 DB。
- 把
local()當多租戶 sandbox:官方文件說它直接使用 host filesystem 與真實程序,設計上沒有隔離。需要不受信任執行時,選真正隔離的環境並最小化 env。
如果你還在整理 Agent 的模型迴圈、工具介面與狀態層,先讀AI Agent Harness 是什麼;若問題已進入憑證外洩與 runtime 強制控制,再接著看AI Agent Secret 安全與前面的 runtime controls。三篇分別處理結構、秘密與權限,不能互相替代。
Flue 2 Agent Hooks 的 8 個常見問題
1. 這裡的 Agent Hooks 指所有框架嗎?
本文特指 Flue 2 的專屬 API。其他 framework 也可能用 middleware、graph state 或 policy layer 做類似能力組裝,但名稱相似不代表 render timing、state scope 與安全邊界相同;請依各自文件驗證。
2. 未掛載的工具還能被模型呼叫嗎?
不能由該次 provider model call 的 tools array 正常選取或呼叫。因為受保護工具的 schema 沒有交給那次 provider request;但 framework 的 task tool 仍會存在,沒有 subagent 時它是 inert,而且系統若另有未受保護的 HTTP、shell 或 MCP 路徑也可能繞過,所以不能把它寫成完整授權保證。
3. setState 後,工具會立刻在同一批呼叫出現嗎?
不會。usePersistentState setter 不會觸發 mid-run render;工具批次 settle 並提交狀態後,下一次 model call 前 render 才看見新值。
4. Flue hooks 也不能條件式呼叫嗎?
多數 resource hooks 可以。useTool 與 usePersistentState 可條件式、可換順序;但 useModel 每次 root render 必須恰好一次,useSandbox 每次最多一次,useDataWriter 名稱則要保持一致。
5. usePersistentState 可以當登入 session 嗎?
不能單獨使用。它是 conversation/agent instance 的 durable state,不會替你驗證 HTTP caller、conversation ownership、session expiry 或權限撤銷。
6. 切換 state 後可以立即換模型嗎?
要到下一個 submission。模型與 thinking/compaction 選項是 submission-scoped;工具在這次 response 改 state,不代表同一 response 後半段立即改用另一個模型。
7. 掛上 local() 就有安全 sandbox 嗎?
沒有隔離。local() 適合已由 container/VM 隔離的開發、CI 或自管自動化;不要把它當不受信任使用者或多租戶的邊界,也不要把整份 process.env 交給模型 shell。
8. 正式化前,這個 lab 先做哪個測試?
先做「未授權工具不可見+即使直打 run 仍被拒絕」。第一個 assertion 測 capability presentation,第二個測真正 authorization;再補 401、403 與 DB restart 恢復,就形成本文 auth/state 範圍的最小測試閉環,仍不代表整體已可上線。
給新手的 5 個重點
- 記住錨點:Agent 當輪能力=render(可信狀態)。
- 用
usePersistentState保存短效、可驗證的 grant,不保存永久登入布林值。 - 條件式
useTool負責縮小模型能力面;route auth 與 tool-run reauthorization 負責真正安全。 - 測工具集合與 render contract,不要誤套 React 的位置規則。
- 持久化 conversation、隔離 sandbox、保護 secret 與授權副作用是不同層,逐層驗收。
想把這個 lab 變成正式服務,可以先把三層閘門畫進你的Agent Harness 設計圖,再用真正隔離的 sandbox限制檔案與程序。若你希望系統化練習軟體架構與實作,也可以到 AlphaLab 線上課程查看完整學習路線。
接著閱讀
左右滑動查看更多推薦
結語:先讓能力變小,再讓執行路徑可強制
Flue 2 Agent Hooks 最有價值的地方,不是把所有安全問題塞進 hooks,而是讓「這一輪到底交給模型哪些能力」變成可閱讀、可測試的程式碼。今天先完成能力與 state 的小閉環:guest render 的 custom tools 沒有私人工具,短效 grant 通過後下一輪才掛載,工具執行時仍重查授權;route 的匿名/越權拒絕則留待你實作 auth 並跑完 HTTP integration tests 後確認。
做完後再回到那句錨點:Agent 當輪能力=render(可信狀態)。當每個等號右邊都有來源、有效期、撤銷與測試,你才不是只把工具藏起來,而是開始建立一個可預測、可恢復、也比較難越權的 Agent。
