Tool 應把 Agent 視為哪一種 security principal,才能同時識別 runtime 與它所代表的人?
想像公司有一個「差旅報帳 Agent」。Alice 把發票丟進對話框,說:「幫我查這筆差旅是否符合規定,符合的話就送出報帳。」Agent 讀取差旅政策、查詢 Alice 的部門與預算,最後呼叫 submit_expense Tool。
Tool 收到請求時,究竟該認誰?
若硬要三選一,三個答案都不完整。比較精確的說法是:Agent 在部署與執行上是 workload,在 OAuth 互動中可能扮演 client;當它代表 Alice 行動時,Alice 仍是 delegated subject。 它不是 Alice,也不能因為被稱作「應用程式」就省略實際 runtime 的身分。
這篇的答案因此不是替 Agent 選一個帳號類型,而是拆開三個問題:
只有 Tool 同時看見這些不同層次,才有足夠資訊判斷這一次 Action。
傳統 web application 的執行路徑通常相對固定:使用者按下「送出」,後端依既定程式呼叫 API。Agent 則先把自然語言 intent 轉成計畫,再動態選 Tool、組 parameters,甚至連續執行多個 Action。同一個 Agent deployment 也可能同時處理許多使用者和 task。
這帶來一個容易被忽略的新邊界:規劃 Action 的邏輯身分、真正發出呼叫的 workload,以及被代表的使用者,不再是同一件事。
OAuth 2.0 本來就把 resource owner 與 client 分成不同角色;client 是在 resource owner 授權下代表其存取 protected resource 的應用程式,而且這個詞不限定它跑在 server、desktop 或其他裝置。也就是說,OAuth 的 application/client 語彙能描述部分關係,卻不會自動回答「這次是哪個 Agent instance 在哪個 task 中選了這個 Tool」。參見 RFC 6749, Section 1.1。
Agent 還會讓 identity context 在執行途中改變。例如同一個 runtime 先用自己的 authority 讀公開政策,接著代表 Alice 查她的費用,之後又遇到需主管批准的高風險 Action。若系統只有一個 principal 欄位,這些責任會被壓成一團。
所以本篇不是在比較「human account、service account 哪個比較好」,而是在問:當 Agent 自主決定下一個 Action 時,Tool 要如何分辨 delegator、actor 與執行 instance,避免把身分驗證成功誤當成完整授權?
最直覺的作法,是讓 Agent 把 Alice 的 access token 原封不動送給 Tool:
{
"iss": "https://identity.example",
"sub": "user:alice",
"aud": "expense-api",
"scope": "expense.submit"
}
Tool 驗證後知道 Alice 可以送出報帳,卻不知道:
這時只要任一可存取 token 的 Agent runtime 被入侵,Tool 看到的仍然只有 Alice。Audit log 可能很完整地記下「Alice 送出報帳」,卻精確地記錯執行者。
反過來,只使用 Agent 的 service identity 也有問題:
{
"iss": "https://workload-issuer.example",
"sub": "agent:expense-assistant",
"aud": "expense-api"
}
現在 Tool 知道 caller 類型,卻不知道它正在代表 Alice 還是 Bob,也無法把 resource ownership、使用者 consent 或部門規則放進 policy。若所有 instance 再共用同一個值,instance 隔離與精準撤銷也一起消失。
兩種設計的共同錯誤,是試圖讓單一 sub 同時表示「受益或被代表的人」與「目前持有並使用 credential 的 actor」。sub 並不是「把所有相關人物塞進來」的欄位;在 OIDC 中,它和 iss 一起用來穩定識別 End-User。參見 OpenID Connect Core 1.0, Section 5.7。
User Identity 識別 Alice,讓 policy 能回答「她是否可對這筆費用發起報帳」。但 Alice 的權限是 Agent 可取得 authority 的上限之一,不是 Agent 的 runtime identity,也不代表每個自動化 Action 都應完整繼承她的權限。
在 delegated Action 中,可以把 Alice 視為 subject:Action 是為她執行、作用在她有關的資源上。之後幾天會再細談 delegation 的範圍;今天先抓住一件事:保留 subject,不等於隱藏 actor。
企業通常需要一個穩定的 Agent definition,例如:
agent_id: expense-assistant
若採 OAuth,這套應用也可能有 client_id。但 client_id 回答的是「哪個 client 取得或使用這項授權」;它不必然等於當下 workload 的密碼學身分,更不等於使用者。RFC 8693 也把 client_id 定義為請求 token 的 OAuth client identifier,和 actor claim 分開表達。參見 RFC 8693, Section 4.3。
此外,Agent definition 本身多半是 control-plane object,不一定直接發出網路請求。把「Agent 名稱」放進 header,只是自我宣告;只有 trusted issuer 將它與經驗證的 deployment / workload 綁定,Tool 才能把它當成 policy input。
Agent 最終仍由 process、container、VM 或其他 runtime 執行。因此在 data plane 上,它首先是一個 workload。Workload identity 應讓接收端驗證:「這個 caller 是企業信任域內、被核准部署的報帳 Agent workload。」
SPIFFE 提供一個 vendor-neutral 的具體參考:SPIFFE ID 是 trust domain 內識別 workload 的 URI;SVID 則是 workload 向 resource 或 caller 證明該身分的可驗證文件。參見 SPIFFE ID / SVID specification。例如:
spiffe://corp.example/agents/expense-assistant
這個 ID 不應塞入人類名稱。它回答的是「哪個 workload」,不是「代表哪個 User」。SPIFFE 也刻意讓 path 的語意由組織定義;因此把 deployment、environment 或 instance 全編進 URI 並非唯一做法,應依隔離與 credential lifecycle 決定。
「報帳 Agent」其實至少有三層:
| 層次 | 例子 | 適合回答 | 是否一定是獨立 principal |
|---|---|---|---|
| Agent definition | expense-assistant |
產品用途、owner、核准 Tool | 不一定;常是治理物件 |
| Deployment | prod/taipei/v3 |
環境、版本、policy profile | 視隔離與 credential 邊界而定 |
| Runtime instance | instance-7f3a |
哪次啟動、哪個執行個體 | 不一定;至少要能關聯 credential 與 audit |
若每個 replica 共用同一 workload identity,Tool 仍可透過可信的 instance_id 做調查,但未必能單獨撤銷某一個 instance。若風險要求 per-instance revocation,就必須讓 credential issuance 和 instance attestation 有更細粒度的綁定;光在 JWT 加一個字串不會創造可信身分。
換句話說,principal 粒度要對齊 enforcement 與撤銷需求。不是每項 metadata 都要變成帳號,但每個拿來作授權的 attribute 都要有可信來源。
RFC 8693 提供了很有用的 delegation 語彙:subject_token 表示「代表誰請求」,actor_token 表示「authority 被交給誰行使」;在 JWT 中,act claim 可以識別目前 actor。
套到本例,可形成這樣的 composite context:
{
"iss": "https://authorization.example",
"sub": "user:alice",
"act": {
"sub": "spiffe://corp.example/agents/expense-assistant"
},
"client_id": "expense-assistant",
"aud": "expense-api",
"scope": "expense.submit",
"agent_deployment": "prod-tpe-v3",
"agent_instance": "instance-7f3a",
"task_id": "task-20260812-0042"
}
這不是建議所有系統照抄的 token schema。agent_deployment、agent_instance、task_id 是本文示意的 private claims;實際部署也可能用 token introspection、mTLS identity、request context 或 policy service 組合這些資料。重點是語意分離:
sub:被代表的 User。act.sub:目前執行 authority 的 Agent workload。client_id / agent_deployment:受治理的應用與部署資訊。agent_instance / task_id:這次 runtime 與任務的關聯資訊。aud / scope:token 的接收對象與授權範圍;它們仍不是針對具體 Action 的完整 policy。RFC 8693 特別提醒,act 內的 claims 用來識別 actor,不應把 exp、nbf、aud 等 token validity claims 塞入其中;接收端仍要驗證最外層 token。參見 RFC 8693, Section 4.1。
User Token ──> Agent ──(沿用 User Token)──> Tool ──> Resource
此設計把 User Identity 當 caller identity,Tool 無法分辨 Agent 與使用者直接操作;Agent 也可能把 broad user token 帶往原本不該接收它的 Tool。另一個常見變形是只用一個固定 service account,讓所有使用者、deployment 與 instance 都投影成相同 principal。

這裡的關鍵不是一定要使用 JWT 或 SPIFFE,而是兩條證據流在 Authorization Service / Tool Gateway 匯合:
NIST Zero Trust Architecture 強調,不應只因網路位置或資產所有權就給予 implicit trust,且 subject 與 device 的 authentication / authorization 是進入 enterprise resource 前的獨立功能。對 Agent 而言,可推導出同樣的原則:**「它跑在公司環境」不能取代可驗證 workload identity,更不能取代逐 Action policy。**參見 NIST SP 800-207。
本例至少有四個邊界:User session、Agent runtime、Authorization / Policy plane、Tool / Resource。每次跨界都不能相信 Agent 自報的 user_id、agent_id 或 instance_id;PEP 必須知道哪些欄位來自 User IdP、workload issuer、Agent registry 或自己的 request context。
submit_expense 時,同時提出 User delegation 與自己的 actor proof。expense-api 使用的短效 context。PDP 不應只問 scope 是否包含 expense.submit,而應至少評估:
subject = user:alice
actor = spiffe://corp.example/agents/expense-assistant
deployment = prod-tpe-v3
instance = instance-7f3a
task = task-20260812-0042
action = submit_expense
resource.owner = user:alice
amount = 12800
可能的 policy 是:Alice 可送自己的費用;actor 必須是 production 的核准報帳 Agent;deployment 版本不得在 denylist;金額超過門檻則要求 human approval。這也說明 authentication、delegation 與 authorization 是三件不同的事:知道 Alice 和 Agent 是誰,仍不代表這一筆 12,800 元的 Action 必須被允許。
這個 PoC 不建立 IdP,也不解析 JWT;它只用 Python standard library 比較三種已解碼 claims set,檢查 Tool 的 audit event 能否同時回答 User、Agent、deployment、instance 與 task。真正系統必須先完成 signature、issuer、audience、time validity 與 credential binding 的驗證,不能直接相信下列 dictionary。
將以下內容存成 identity_layers.py 後執行 python3 identity_layers.py:
claim_sets = {
"user_only": {
"sub": "user:alice",
"aud": "expense-api",
"scope": "expense.submit",
},
"agent_only": {
"sub": "spiffe://corp.example/agents/expense-assistant",
"aud": "expense-api",
"client_id": "expense-assistant",
},
"layered": {
"sub": "user:alice",
"act": {
"sub": "spiffe://corp.example/agents/expense-assistant"
},
"aud": "expense-api",
"scope": "expense.submit",
"client_id": "expense-assistant",
"agent_deployment": "prod-tpe-v3",
"agent_instance": "instance-7f3a",
"task_id": "task-20260812-0042",
},
}
def audit_view(claims):
actor = claims.get("act", {}).get("sub")
fields = {
"represented_user": (
claims.get("sub")
if claims.get("sub", "").startswith("user:")
else None
),
"direct_actor": actor,
"agent_app": claims.get("client_id"),
"deployment": claims.get("agent_deployment"),
"instance": claims.get("agent_instance"),
"task": claims.get("task_id"),
}
missing = [name for name, value in fields.items() if value is None]
return "complete" if not missing else "missing: " + ", ".join(missing)
def authorize(claims):
expected_actor = "spiffe://corp.example/agents/expense-assistant"
if claims.get("aud") != "expense-api":
return "DENY: wrong audience"
if claims.get("sub") != "user:alice":
return "DENY: no delegated user"
if claims.get("act", {}).get("sub") != expected_actor:
return "DENY: unverified Agent actor"
if claims.get("agent_deployment") != "prod-tpe-v3":
return "DENY: unapproved deployment"
return "ALLOW: subject and actor are independently visible"
for name, claims in claim_sets.items():
print(f"{name:10} | audit={audit_view(claims)}")
print(f"{'':10} | decision={authorize(claims)}")
預期輸出:
user_only | audit=missing: direct_actor, agent_app, deployment, instance, task
| decision=DENY: unverified Agent actor
agent_only | audit=missing: represented_user, direct_actor, deployment, instance, task
| decision=DENY: no delegated user
layered | audit=complete
| decision=ALLOW: subject and actor are independently visible
PoC 證明的不是「claims 越多越安全」,而是沒有 principal separation,Tool 就連寫出正確 policy 與 audit event 的材料都沒有。反過來,欄位齊全也不代表可信;production PEP 還必須驗證每個欄位的 provenance,並只接受由可信系統綁定的 context。
這個 pattern 不要求特定平台。User Identity 可由支援 OIDC 的 IdP 提供;workload identity 可採 SPIFFE / SPIRE、平台原生 workload identity 或具等價 attestation 性質的機制;composite context 可由 OAuth Token Exchange、token introspection 或 policy gateway 實作。選型時應先問「能否分開 subject 與 actor、能否驗證 runtime、能否限制 audience 與 lifetime」,再比較產品名稱。
sub 不能同時模糊地代表 User 與 Agent;代表誰和誰在執行是兩個不同問題。今天把 User、Agent application 與 workload runtime 拆開了,下一個問題也隨之出現:如果企業為了方便,讓所有 Agent workload 又共用同一組 Service Account,這些剛建立的邊界是否會再次被壓平?Day 3 將從 blast radius、撤銷與歸責三個角度回答:為什麼不能讓所有 Agent 共用一組 Service Account?