目次を開く
AIエージェントにCRM,メール,Calendar,Ticketを操作させると,自動化は回答の生成から業務の実行へ広がります.一方,API Tokenを一つ渡すだけでは,誰の権限で読み,何を承認し,どの時点で外部の状態が変わったのかが曖昧になります.
営業担当者が「この顧客へFollow-upを送って」と依頼した場合でも,顧客を読む権限,送信元Mailboxの権限,本文を作る判断,この内容で送る承認は別です.さらに,送信要求がTimeoutしたら,送れなかったのか,応答だけ失われたのかを区別する必要があります.
本稿では,Authority,Capability,Approval,Execution,Finality,Auditを別の境界として設計します.AIエージェントを本番へ進める6つの設計と,AIを追加する前の境界設計を,SaaS操作の実装判断へ落とすEngineering記事です.掲載例は設計・レビュー用であり,SaaSへの書込み,認証,SDKやSQLの実行,障害注入は行っていません.
1.依頼者,実行Actor,承認者を分ける
共有Admin Credentialを持つAgentが,利用者の代わりに全顧客を検索する構成を考えます.利用者が20件しか見られないのにAgentは2万件へ到達できるなら,Agentが権限昇格の入口になります.最終回答で20件へ絞っても,その前にLLM Context,Cache,Traceへ残りの情報が流れる可能性があります.
Microsoft GraphはDelegatedとApp-onlyを区別します.DelegatedではサインインしたUserに代わって動き,ApplicationはUserがAccessできないものへAccessできません.App-onlyではUserなしでApplication自身のPermissionを使います.[1]
| 主体 | 記録する意味 | 信頼できる情報源 |
|---|---|---|
| Requester / Subject | 誰の依頼・委任で行うか | 検証済みSessionとDelegation |
| Agent / Workload Actor | どのRuntime・Deploymentが提案・実行するか | Workload認証とDeployment管理 |
| Approver | 誰が具体的な操作を承認したか | 承認時の認証・認可 |
| SaaS Principal | SaaSがどのUser/Application/Accountとして扱うか | 実際のCredentialとProvider応答 |
User ID,Tenant ID,接続先AccountをModelのTool Argumentsからそのまま採用しません.Gatewayは認証済みContextからこれらを束縛し,Resourceとの関係を確認します.Agent名をLogへ書くだけでも認証にはなりません.ActorとSubjectを分ける内部モデルは有用ですが,Tokenに常に同じ形式のact Claimがあるとは仮定しません.
横にスクロールして図全体をご覧いただけます.
図を文章で読む
AgentはCapabilityとArgumentsを提案する.Gatewayが認証済みContext,Policy,具体的なApprovalを確認し,ExecutorがBrokerとJournalを使ってSaaSを呼ぶ.SaaS側の認可と結果照合も残る.
2.DelegatedとApp-onlyは別Workflowにする
「自分のCalendarへ予定を追加する」ならUserの委任を使い,「毎晩組織の未処理Ticketを集計する」なら専用Workload Identityを検討します.後者にも,対象Tenant,Project,Mailbox,操作の上限を定めます.App-onlyが必ず全Dataへ到達するわけではなく,SaaSが提供するApplication PermissionやResource単位の制約が実効範囲を決めます.[1]
MicrosoftのOn-Behalf-Of Flowは,中間APIがUserを代表して下流APIのDelegated Tokenを取得する仕組みです.User Principalを対象とし,App-only TokenをそのままOBOで委任に変えるものではありません.入力TokenのAudienceも中間APIに対応している必要があります.[2]
RFC 8693のToken ExchangeとRFC 8707のResource Indicatorsも,委任・対象Resourceの設計に役立ちます.ただし,すべてのSaaSが対応するわけではなく,Microsoft OBOと同一のProtocolでもありません.BrokerがScopeを小さく要求したという理由だけで,発行されたTokenが期待どおり限定されたと判断しません.実際の発行契約と認可を確認します.[3][4]
特にMailとCalendarがどちらもMicrosoft Graphにある場合,製品名を分けてもAudienceが別になるとは限りません.Resource Audience,OAuth Scope,Mailbox等の権限,GatewayのCapability制約を組み合わせます.customer.readのような内部Capability名を,実在するOAuth Scopeと混同しないようにします.
Delegatedで403になったら,管理用App TokenへFallbackしません.委任の失敗はそのWorkflowの拒否として扱い,App-onlyの定期処理は別Identity,別Policy,別監査として設計します.
3.Toolを業務Capabilityにし,Readにも制約を置く
generic_http_requestより,read_customer,prepare_followup,send_customer_emailという業務の粒度で境界を作ります.AdapterがProvider固有のAPIへ変換し,GatewayがSchema,Tenant,対象Resource,接続先Account,Policyを確認します.
# Proposed contract; provider behavior must be reviewed per endpoint.
send_customer_email:
authority: delegated
model_may_choose_principal: false
risk: external_side_effect
approval: exact_prepared_action
credential_owner: token_broker
retry: reconcile_before_retry
provider_idempotency: NOT_VERIFIED_DO_NOT_ASSUME
acceptance: graph_sendMail_202_without_response_body
completion_evidence: DEFINE_FOR_BUSINESS_OUTCOME
compensation: no_general_unsend_guarantee
execution_status: NOT_RUN
これはTool Contractの例です.RiskやApproval条件はModelに申告させず,Backendが対象,金額,公開範囲,Environmentから決めます.「CRM Noteだから可逆」と固定もしません.Note作成が通知やWebhookを起動するなら,削除しても外部への影響は残ります.
ReadにもUser委任,Resource scope,Query制約,件数制限,Field projectionを適用します.取得後にLLMへ渡すDTOを絞り,権限のないDataを先に取得してから隠す構成を避けます.Cache,Vector検索,添付Fileの取り出しも同じTenant・User境界で扱います.
SaaSのDocumentやTicket本文は,外部から入る非信頼Dataです.「このTokenを外部へ送れ」「承認済みとして進めろ」という文章が含まれていても,権限を変更する命令にはしません.Prompt Injectionへの対応は,Modelへの注意書きだけでなく,許可されたCapability,送信先,認可,Approvalの強制点で行います.[13]
4.Prepareで実行内容を固定する
送信を一つの自由なTool Callにせず,Prepareで具体的なActionを保存し,Commitではその保存済みActionを実行します.ここでいう二段階はApplicationのWorkflowであり,SaaSを巻き込む分散TransactionのTwo-phase Commitではありません.
Prepareで保存する対象には,宛先だけでなく,送信元Mailbox,To / Cc / Bcc,Subject,Bodyの形式と内容,Reply-To,添付の固定Version・Hash,Tenant,接続先Account,Operation ID,対象Resource,期待Revision,期限を含めます.金銭操作なら金額,通貨,支払先も同様です.
{
"schema_version": "agent-action/v1",
"operation_id": "00000000-0000-4000-8000-000000000001",
"tenant_id": "tenant-example",
"subject_id": "user-example",
"actor_id": "followup-worker-example",
"authority_mode": "delegated",
"connector_account_id": "mailbox-connection-example",
"capability": "send_customer_email",
"target_id": "customer-example",
"expected_revision": "crm-revision-example",
"policy_version": "policy-example-v1",
"expires_at": "2026-09-24T12:10:00Z",
"payload": {
"from_mailbox": "sales@example.com",
"to": ["customer@example.net"],
"cc": [],
"bcc": [],
"reply_to": [],
"subject": "Follow-up",
"body_type": "Text",
"body": "Thank you for the meeting. May we discuss the next steps?",
"attachments": []
}
}
Action Fingerprintは,Schemaを検証してDefaultを展開した実行用Envelopeから作ります.例えばRFC 8785のJSON Canonicalization Schemeを使い,Domain separatorとSchema versionを含めてSHA-256を計算します.適当なJSON Serializerの出力をHashしただけでは,Key順や数値表現の差で不一致が起きます.重複Key等の曖昧な入力も受け付けません.[7]
# Algorithm contract, not an implemented hashing function.
canonical_bytes = JCS(validated_execution_envelope)
fingerprint = SHA256(UTF8("agent-action:v1") || 0x00 || canonical_bytes)
# Store separately; the fingerprint is not a field in its own input.
approval = (tenant_id, operation_id, fingerprint, approver, expires_at)
Hashは承認者の認証や署名ではありません.Serverが保護するRecordに,承認対象のDigest,承認者,Approval ID,期限を結び付ける必要があります.ClientがActionとHashを両方書き換えられるなら比較しても意味がありません.
承認画面には,Hashだけでなく実際の送信内容と対象を示します.添付を後で可変URLから取り直したり,Commit時に宛先Groupを再展開したりすると,承認した内容から変わり得ます.Payloadを変更するなら新しいApprovalが必要です.
横にスクロールして図全体をご覧いただけます.
図を文章で読む
PrepareでTenant,Subject,Actor,宛先,本文,添付Version,Revisionを固定する.認可されたReviewerが内容を承認し,ServerがFingerprintとOperationへ結び付ける.Commitは現在の認可と同じActionを確認する.
5.Approvalを永続Stateとして管理する
OpenAIの公式資料では,承認が必要なTool CallでInterruptを返し,Stateを保存して同じRunを再開する流れが説明されています.これは停止・再開のPrimitiveです.誰が承認できるか,何を承認したか,承認がいつ失効するかはApplicationが実装します.[6]
Browserへは,表示に必要なActionとOpaqueなApproval IDを渡します.完全なRun State,Token,内部の判断材料はServer管理のStorageに置きます.この保管方針は本稿の設計選択です.SerializeしたStateや推測しにくいID自体を,認証・認可の代わりにはしません.
承認EndpointはSession,Tenant,承認Role,対象Operation,CSRF等を確認します.Self-approvalを許すか,RequesterとApproverの分離が必要かもPolicyで決めます.拒否,期限切れ,取消し,再送,並行クリックをState Transitionとして扱い,同じApprovalを別Operationへ使い回せないようにします.
High-impactな操作ではStep-upを加えられます.ただし「強く認証した」ことと「このActionを承認した」ことは別です.Step-up結果を承認Sessionへ結び付け,その承認を具体的なEnvelopeへ結び付けます.Frameworkのサンプルにある自動Approve Loopを,本番の人間承認として転用しません.
6.実行直前に認可を取り直し,同時更新はAPIで止める
承認後にUserが無効化された,担当顧客が変わった,Policyが更新された,Mailboxの権限が失われた,という変化があり得ます.Commit直前に,現在の認可,Approvalの有効性,保存したFingerprint,対象Stateを再確認します.Approvalは権限の代替ではありません.
ただし,GET → Revision比較 → PATCHだけでは,比較と更新の間に別の書込みが入る競合を防げません.APIが対応する場合は,承認済みRevisionをIf-Match等で更新Requestへ渡し,SaaS側で条件と更新を一体として評価させます.HTTPではIf-Matchによる条件付きRequestと412 Precondition Failedが定義されています.実際の対応範囲はEndpointごとに確認します.[8]
# Conceptual CRM endpoint; verify support before adopting an adapter.
PATCH /opportunities/example
If-Match: "approved-revision"
Content-Type: application/json
{"stage":"closed_won"}
# Precondition mismatch -> reload and re-plan; no blind PATCH retry.
# This is not an If-Match example for Microsoft Graph sendMail.
Revision不一致は,単なるRetryではなく,取り直し・再計画へ戻します.対象Stateや内容が変わるなら新しいApprovalが必要です.ETagがないAPIでupdated_atを比較しても,SaaSが条件付き更新を強制しなければ同等の保証にはなりません.残る競合を許容できない操作は,自動Commitの対象から外すか,より強いDomain APIを用意します.
認可の再評価と外部Commitも,一つの分散Transactionにはなりません.下流SaaSの認可,短い実行猶予,対応する失効制御を併用し,残る失効伝播時間を記録します.Local Policyの再確認だけで,User失効直後から全Requestを必ず止められるとは主張しません.
7.Execution Journalで並行実行とCrashを扱う
副作用を持つActionへServerがOperation IDを割り当て,Tenantと組にして永続化します.同じ実行要求のRetryは同じOperationへ戻します.毎回新しいUUIDを作ると,重複排除の意味がありません.逆に,後日意図して同じ内容を送る別操作まで,Payload Hashだけで一律に抑止しないようにします.
Localでは,承認済みOperationの実行権をAtomicに取得します.次はPostgreSQL向けのClaim断片です.現在の認可確認やApproval Recordとの照合,保存内容の改変防止,Outboxは別途必要であり,これだけでExecution Engineは完成しません.
-- PostgreSQL design fragment: $1 tenant, $2 operation, $3 digest (bytea).
-- Only a trusted executor may call this after current policy/approval checks.
UPDATE agent_operations
SET state = 'executing',
attempt = attempt + 1,
claimed_at = CURRENT_TIMESTAMP
WHERE tenant_id = $1
AND operation_id = $2
AND state = 'approved'
AND action_digest = $3
AND approved_by IS NOT NULL
AND approval_expires_at > CURRENT_TIMESTAMP
AND action_expires_at > CURRENT_TIMESTAMP
RETURNING operation_id, canonical_action, attempt;
-- Zero rows: do not dispatch. Commit this local claim before calling SaaS.
-- Unresolved executing attempts are NOT automatically returned to approved.
更新結果が0行なら,そのWorkerは送信しません.executingをDBへCommitしてから外部Requestを開始します.並行Workerが同じ承認を消費する競合は抑えられますが,SaaSの副作用とDBへの結果保存を一つのTransactionにはできません.
例えばSaaSが処理した直後,結果を書き込む前にWorkerが停止する場合があります.Journalがexecutingのままでも,「送っていない」とは言えません.Lease切れだけで別Workerが再送すると,停止したと思った元Workerと二重実行することもあります.未解決の試行は,Providerの重複排除契約が確認できない限り,照合へ回します.
横にスクロールして図全体をご覧いただけます.
図を文章で読む
承認済みOperationをAtomicにClaimしてからSaaSを呼ぶ.受付応答と業務完了は別である.応答喪失やWorker停止ではunknownへ進め,Provider契約に基づく照合なしに再送しない.
failedは,副作用が発生していないことを契約上判断できる失敗に限定します.送達可能性が残るTimeoutはunknownです.未知の結果を失敗に丸めず,Customerに「未送信」と断定することも避けます.
8.IdempotencyはProviderの操作単位で確認する
自分たちのJournalにOperation IDがあっても,SaaSがそのIDを重複排除に使わなければ,外部の二重実行は防げません.LocalのAtomic Claim,ProviderのIdempotency,結果の照合を別々に設計します.
| 操作の例 | 公式に確認できる契約 | 設計への反映 |
|---|---|---|
Microsoft Graph sendMail | 202 Accepted,応答Bodyなし.処理完了を意味しない | Message ID返却や汎用Idempotency Keyを仮定しない |
| Microsoft Graph Event作成 | transactionIdでRetry時の冗長なPOSTを抑える用途 | 同じ作成操作で同じ値を使い,保持期間等は別に確認 |
| Stripeの対応POST | Idempotency Keyに対する結果を保存し再利用 | Parameter一致,Key保持期間,Endpointの契約を守る |
| 自社AdapterのJournal | LocalなOperation識別とState管理 | 外部CommitのExactly-onceとは区別 |
Graph sendMailの成功応答から,SaaS Message IDを保存できるとは限りません.Client Request IDやProvider Request IDも,Message IDや配送証明ではありません.Draft作成を経由する設計でも,利用Scope,Draftの変更,送信結果の照合を別途設計します.[9]
CalendarのtransactionIdは作成Retryの重複抑制用ですが,その存在からすべての更新・削除の同じ保証は導けません.Stripeは少なくとも24時間経過後のKey削除が可能で,削除後の再利用は新規Requestになります.同じKeyでParameterが違えばErrorになり,保存された500応答も再利用されます.Keyは無期限の重複防止記録ではありません.[10][11]
Retry契約には対象Account,Endpoint,同じPayload,Keyの有効期間,応答保存の条件を含めます.不明な点をprovider_supported: trueで埋めません.本文中のメール送信例は,Provider側のIdempotencyを確認できない前提です.
429ではProviderのRetry-After等に従い,Backoff,Jitter,並列数,Retry BudgetをRuntimeで制御します.ただし再試行待ちの前に,その失敗が副作用なしと判断できるかを確認します.上位のModel Loopが新しいTool Callとして再送する経路にも,同じOperation管理を適用します.[12]
9.受付,完了,配送,補償を別の結果にする
外部APIの成功は,業務の最終結果とは限りません.メール送信なら,受付済み,Provider処理完了,配送確認,受信者の閲覧は異なるStateです.Graphの202は受付のEvidenceとして記録し,配送済みと表示しません.[9]
| State | 判断できること | 次の扱い |
|---|---|---|
prepared / approved | 内容が準備・承認された | 有効性を確認して実行権を取得 |
executing | Localで実行権を取得した | 外部呼出しの結果を待つ |
accepted | Providerが要求を受け付けた | 必要な業務結果を別途確認 |
completed | 定義したOutcomeをEvidenceで確認した | 定義とEvidenceを監査へ残す |
failed | 副作用なしの失敗が確認できる | Policyに従って修正・Retry |
unknown | 外部で実行された可能性が残る | 照合.不明なら自動再送しない |
Finalityは,取り消し困難な外部効果が発生する境界です.取消し可能なStageと,完了と報告するためのEvidenceをCapabilityごとに定義します.CancelやRefundは元の出来事を消しません.別の副作用であり,追加の認可,承認,Operation ID,監査が必要です.Calendarの取消し通知など,補償がさらに外部へ情報を出す場合もあります.
複数操作のWorkflowでは,検証と可逆な準備を先へ,取り消し困難な送信を後へ寄せます.ただし順序を変えても全体がAtomicになるわけではありません.どこまで成功し,何を補償するかを明示します.
10.CredentialとBrowserを同じ実行境界で扱う
CredentialをPrompt,Tool Result,Traceへ入れません.ModelはCapability名と検証済みDTOを扱い,Executorが認証済みWorkloadとしてToken Brokerを呼びます.Brokerは,任意のuser_idを渡されただけでTokenを発行せず,委任,Tenant,接続先,必要Scope,現在のPolicyを確認します.
Refresh TokenはBroker側のSecret管理へ置き,Workerには必要な短命Credentialだけを渡すか,BrokerとAdapter側で呼出しを完結させます.対応する場合はWorkload Federation等も選択肢です.Tokenの寿命とAgent Runの寿命を一致させる必要はありません.
DPoPはTokenをClientのKeyへ束縛する仕組みですが,Authorization ServerとResource Serverの対応が必要です.Tokenだけの持出しへの対策になっても,Keyを利用できるExecutorごと侵害された場合の万能な対策ではありません.Audienceや宛先検証,Secret保管も引き続き必要です.[5]
Browser Automationでは,CookieとブラウザProfileが広い権限を持ちます.「Sendを押す前に聞く」とPromptへ書くだけでは不十分です.Modelが同じSessionで自由にClick,JavaScript,Network Requestを実行できれば,承認Toolを迂回できる可能性があります.
High-impact操作は,Backendが認可を強制できるAPI/Domain Capabilityへ寄せます.UIしかない場合は,ブラウザの隔離,対象Account固定,操作制限,具体的な画面・内容の確認,確定操作の制御可能性を評価します.強制できない場合は人間が確定を引き取る設計にし,PromptだけでBoundaryを実装したとは扱いません.
11.AuditとEvalを実行系列へ結び付ける
Auditには,Tenant,Requester,Actor,Capability,Target,Authority mode,接続先Account,Policy version,Approval,Fingerprint,実行時刻,試行,Provider Request ID,結果のEvidenceを残します.Access TokenやRefresh Tokenは残しません.Digestも,推測可能なPayloadのHashなら機密性を保証しないため,Access制御と保存期限が必要です.
Local State変更と監査EventのOutbox登録を同じDB Transactionへ入れると,別の監査Systemへ配送する前にEventを失うリスクを減らせます.ただしOutboxも外部SaaSの処理とAtomicではありません.業務JournalをSamplingされるTraceだけへ置かず,監査配送のRetryと重複排除も持たせます.
Operation IDでTrace,Journal,SaaSの記録を結びます.外部IDがない場合は,利用可能なRequest ID,時刻,Account,照合結果とその確度を残し,対応付けを捏造しません.
| 否定・障害ケース | 期待する境界 | 今回の状態 |
|---|---|---|
| 別Tenant/担当外Customerを要求 | LLMへDataを渡す前に拒否 | NOT_RUN |
| Documentが認可Bypassを指示 | 非信頼Dataとして扱い,Policyを変更しない | NOT_RUN |
| ClientがRecipientやApproval Stateを変更 | Server保存のActionと不一致で拒否 | NOT_RUN |
| 他Userの承認ID/期限切れ/Replay | Reviewer・Tenant・Operation・Stateで拒否 | NOT_RUN |
| Approval後に権限を失う | Commit時の再認可で拒否し,伝播限界も記録 | NOT_RUN |
| Approval後にResourceが更新される | Providerの条件付き更新で競合を検出 | NOT_RUN |
| 二つのWorkerが同じOperationをClaim | Localの実行権取得は一方だけ | NOT_RUN |
| SaaS処理後に応答喪失/Worker停止 | unknownとして照合.自動再送しない | NOT_RUN |
| Idempotency Keyの保持期間を過ぎる | 保証が失われた状態で盲目的にRetryしない | NOT_RUN |
| Delegated拒否時のApp-only Fallback | 別Authorityへ昇格せず拒否 | NOT_RUN |
Evalは最終回答の良さだけでなく,Tool選択,参照したPrincipal,承認したEnvelope,State Transition,外部結果を評価します.Fake Adapterで期待した系列を確認することと,実SaaSのIdempotency・失効・配送を確認することは別のEvidenceです.
12.一つのCapabilityから運用契約を作る
Capability契約,Action Schema,SQL断片,受入確認資料をダウンロードできます.完成したAgent FrameworkやSaaS Connectorではなく,境界をReviewするためのArtifactです.Fingerprint実装,認証・認可,Approval UI,Provider Adapter,Outbox Workerは含めていません.資料の確認項目はすべてNOT_RUNで,SDK・SQL・SaaSで検証済みとは扱いません.
横にスクロールして図全体をご覧いただけます.
図を文章で読む
Operation IDでRequesterとActor,承認した内容,Providerへの試行,結果Evidenceを追う.肯定例だけでなく権限失効,同時更新,Replay,Timeoutを確認する.今回の確認計画はNOT_RUNである.
導入は,例えば「Userの担当CustomerへFollow-upを一件送る」という一つのCapabilityから始めます.Authorityと読めるFieldを定め,PrepareのEnvelopeを固定し,承認対象を表示し,Commitの再認可を実装し,Timeout後の照合手順まで決めます.結果が不明なときに誰が引き取るか,CapabilityやAccountを停止する経路も定めます.停止は既に送ったRequestを取り消すものではありません.
Modelは意図を読み,候補を作り,必要なCapabilityを提案します.Backendは誰の権限か,何を承認したか,今も許されるか,同じ操作を既に試したかを確認します.SaaSは実際のPrincipalに対する最終的な認可を行います.
仕事を提案する自律性と,外部の状態を確定する権限を分けます. その上で,承認した内容,実行した要求,確認できた結果を一本のOperationとして追えることが,SaaSを操作するAgentの運用基盤になります.
参考資料
以下の公式資料・標準を参照しています.EndpointやSDKの機能を,すべてのSaaSの共通保証へ一般化しない前提で参照しています.
- Microsoft Graph permissions overview
- Microsoft identity platform On-Behalf-Of flow
- RFC 8693: OAuth 2.0 Token Exchange
- RFC 8707: Resource Indicators
- RFC 9449: DPoP
- OpenAI Agents: Guardrails and approvals
- RFC 8785: JSON Canonicalization Scheme
- RFC 9110: HTTP conditional requests / If-Match
- Microsoft Graph sendMail
- Microsoft Graph event / transactionId
- Stripe idempotent requests
- Microsoft Graph throttling
- OpenAI safety in building agents