Day 23 我讓兩個 worker 同時分析不同問題。它們都只有唯讀 prompt,沒有危險工具,所以責任邊界還算單純。
真正麻煩的是 handoff。一個 triage Agent 若把「重設密碼」交給帳號 Agent,下游到底代表誰執行?它能拿到多少權限?最後出事又要算在哪一個 run?
我的做法不是把上一個 Agent 的 API key 一起傳過去,而是建立 server-side handoff envelope。裡面明確保存原始 subject、目前 actor、目標 Agent、task、有效 scope 與 audit run。權限只能縮小,不能因為多了一次 handoff 就變大。
使用者 Prompt
→ triage_agent:Live LLM 提出 HandoffProposal
→ server-side identity 與 operation scope mapping
→ 建立最小權限 handoff envelope
→ identity_specialist:Live LLM 提出 PasswordResetProposal
→ delegated scope 檢查
→ approval gate
→ reset_password_handler skipped
→ audit owner recorded
這次有兩段真實模型呼叫。第一段決定要交給哪個角色,第二段把密碼重設需求整理成待審核 proposal。兩個模型都沒有最終授權權。
LangChain 的 handoffs 文件把它描述成 state-driven behavior:工具更新 current_step 或 active_agent,系統再依狀態切換 prompt、tools 或路由到另一個 Agent。文件也提醒,多 Agent subgraph 必須明確決定哪些 messages 會傳下去,否則容易帶入無關 context 或破壞對話結構。LangChain:Handoffs
我的 demo 沒有把整份對話、上游 token 或所有工具一起複製。下游只收到這次 task,以及伺服器建立的 envelope。
run_id
tenant_id
subject_id
actor_chain
target_agent
task
effective_scopes
這個 envelope 是記憶體中的 mock 物件,不是 JWT,也沒有拿來冒充正式 OAuth token。
我把兩個身分分開:
student-24,系統是替誰處理問題。triage-agent,再到 identity_specialist。RFC 8693 的 OAuth 2.0 Token Exchange 也區分 delegation 與 impersonation,並用 subject 與 actor 表達「誰授權、誰正在代為執行」。它的 act claim 還能保存 delegation chain。RFC 8693:OAuth 2.0 Token Exchange
Day 24 沒有實作 token exchange,但採用同一個基本原則:下游不能把自己說成原始使用者,也不能把 actor chain 洗掉。
triage Agent 的 HandoffProposal 會包含 task 與它認為需要的 scopes。後端不直接採信這個清單,而是先依操作推導 required scopes,再計算交集:
effective_scopes
= requested_scopes
∩ parent_principal.scopes
∩ target_agent.allowed_scopes
這次 parent principal 有 account.read 與 password.reset,但 identity_specialist 的 delegation allowlist 只有 account.read。因此 trace 會留下:
operation_scope_map derived
principal_propagation derived
delegation_scope reduced
handoff_envelope issued
上游有某個權限,不代表它必須下放。target Agent 也不能透過 prompt 要求自己增加 scope。
identity specialist 會真的呼叫模型,將需求整理成 PasswordResetProposal。這只是待審核資料,不會直接呼叫 handler。
後端要同時滿足兩個條件:
password.reset in effective_scopes
AND
approval event 已由正確的人完成
這次兩個條件都不成立,所以結果是:
delegated_scope blocked
approval_gate pending
reset_password_handler skipped
即使使用者在同一段 Prompt 寫「我同意」,demo 也不會把文字當成正式 approval event。下一章才會實作可驗證、可過期且綁定參數的核准流程。
LangChain 的 Human-in-the-loop middleware 也是在工具呼叫符合政策時先發出 interrupt,保存狀態並等待 approve、edit 或 reject,而不是讓模型自己聲稱已獲核准。LangChain:Human-in-the-loop
如果只記錄最後一個 Agent,audit log 會看起來像 identity_specialist 自己發起密碼重設,原始請求與 delegation chain 也跟著消失。
Day 24 把以下資料放在同一個 audit ownership 裡:
RUN-DAY-24
subject: student-24
actor chain: triage-agent → identity_specialist
requested operation: password reset
policy result: blocked / pending
handler result: skipped
因此 trace 最後會出現 audit_owner recorded。即使工具沒有執行,拒絕與等待核准也要留下紀錄。

目前 envelope 只存在單次記憶體執行,沒有簽章、到期時間、audience 或 nonce;approval gate 也固定停在 pending,還不能跨 request 恢復。這些缺口不能拿去保護正式帳號系統。
我現在先固定三條規則:身分由伺服器建立、scope 只能縮小、handoff 不能代替 approval。等下一篇加入真正的核准狀態,再處理一次核准到底能授權多少事。
handoff envelope 由 server-side principal 建立:
effective = (
request.requested_scopes
& principal.scopes
& TARGET_AGENT_SCOPES[request.target_agent]
)
envelope = HandoffEnvelope(
run_id=run_id,
tenant_id=principal.tenant_id,
subject_id=principal.subject_id,
actor_chain=(principal.actor_id, request.target_agent),
target_agent=request.target_agent,
task=request.task,
effective_scopes=frozenset(effective),
)
危險操作需要 scope 與核准同時成立:
def authorize_delegated_operation(envelope, *, required_scope, approved):
has_scope = required_scope in envelope.effective_scopes
decisions = (
PolicyDecision(
has_scope,
"delegated_scope",
"allowed" if has_scope else "blocked",
"envelope 必須包含危險操作所需 scope",
),
PolicyDecision(
approved,
"approval_gate",
"approved" if approved else "pending",
"危險操作需要獨立核准",
),
)
return has_scope and approved, decisions
單元測試會確認未知 handoff target 被拒絕、下游 scope 不可能超過上游與 target allowlist、actor chain 不會遺失,以及缺少 scope 或 approval 時 handler 都不能執行。Promptfoo 的 handoff_scope_and_approval_required 會把 scope 縮減、approval pending 與 handler skipped 綁成同一個回歸案例。Live 測試則確認 triage 與 identity specialist 真的各自呼叫一次模型,最後停在 approval_gate pending。
下一篇會把畫面上的「確認」變成有範圍的授權,而不是一顆按下去什麼都能做的按鈕。