iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0

Day 23 我讓兩個 worker 同時分析不同問題。它們都只有唯讀 prompt,沒有危險工具,所以責任邊界還算單純。

真正麻煩的是 handoff。一個 triage Agent 若把「重設密碼」交給帳號 Agent,下游到底代表誰執行?它能拿到多少權限?最後出事又要算在哪一個 run?

我的做法不是把上一個 Agent 的 API key 一起傳過去,而是建立 server-side handoff envelope。裡面明確保存原始 subject、目前 actor、目標 Agent、task、有效 scope 與 audit run。權限只能縮小,不能因為多了一次 handoff 就變大。

今天真正呼叫 LLM 的位置

使用者 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。兩個模型都沒有最終授權權。

Handoff 是狀態轉移,不是權限複製

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。

Subject 和 Actor 不能混成同一個欄位

我把兩個身分分開:

  • subject:原始使用者 student-24,系統是替誰處理問題。
  • actor:目前執行工作的 Agent,先是 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 洗掉。

模型可以要求 scope,不能自己核發 scope

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。

Handoff 完成,仍不等於核准完成

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。即使工具沒有執行,拒絕與等待核准也要留下紀錄。

image

這個 demo 還缺什麼

目前 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。

下一篇會把畫面上的「確認」變成有範圍的授權,而不是一顆按下去什麼都能做的按鈕。

本日程式碼

day-24-handoff-permission


上一篇
Day 23|多 Agent 真的比較強嗎?我先看到的是更多安全洞
下一篇
Day 25|確認按鈕按下去後,AI 到底被授權做多少事?
系列文
從 LLM、Agent 到 Guardrails:30 天打造可控、安全、可驗證的 AI Agent 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言