Blog · · 10 min read
AI agent separation of duties for approval workflows

AI agent separation of duties fails when a product pauses for approval but cannot prove who proposed, approved, and executed the action. A requester may approve their own deletion request. Two agents may pass authority between them. A stale group membership may satisfy a quorum after the policy changed. The interface still shows an approval, yet one effective principal controlled the whole operation.
Fix this by binding the action, initiating identity chain, policy version, approver rules, decisions, and execution permission into one durable record. The result is an approval workflow that rejects self-approval, requires independently eligible people, invalidates changed actions, and gives the executor a short-lived grant for one exact effect.
Use one contract from proposal to execution
Treat approval as an authorization protocol with six persisted stages:
- Proposal: Canonicalize the intended action and record who initiated it, including every delegated agent in the chain.
- Approval subject: Freeze the exact action, target, resource version, risk class, policy version, and eligibility predicates under one digest.
- Eligibility check: Resolve each approver through a trusted identity provider and evaluate current roles, groups, conflicts, and credential state.
- Quorum decision: Count only distinct, eligible approvals that match the frozen subject. Record denials and abstentions too.
- Execution grant: Mint a short-lived, single-use authorization scoped to the approved subject.
- Receipt: Commit the grant consumption and effect receipt together, then preserve enough evidence to reconstruct the decision.
This contract implements the control objective behind NIST SP 800-53 AC-5, which requires organizations to identify duties that need separation and define access authorizations that enforce that separation. It also fits OWASP MCP02 guidance on unique agent identities, least privilege, just-in-time elevation, approval gates, and immutable attribution.
The six stages must share one immutable subject identifier. A generic approved: true field cannot show which payload, resource version, requester, agent chain, or policy was approved.
Why a normal approval loop is incomplete
Most agent frameworks provide a useful pause-and-resume primitive. The Microsoft Agent Framework approval documentation shows how a tool can require approval, how the request carries tool arguments, and how the workflow resumes in the same session after a response. That mechanism answers whether the run may continue. Your product still has to decide who qualifies as an independent approver and whether the resumed execution matches the approved action.
Four gaps create false separation:
- Identity collapse: The user, operator, service account, parent agent, and child agent appear as one runtime identity. The policy cannot tell whether the approver also initiated or authored the action.
- Mutable approver selection: Workflow input names a reviewer or group. An agent can steer approval toward a permissive principal instead of using a server-owned policy.
- Decision-time drift: Group membership, employment status, resource state, and policy change while the run waits. Eligibility checked only when the request was created becomes stale.
- Grant reuse: An approval is stored as a durable boolean or broad token. A retry, edited payload, or second tool call can reuse it for a different effect.
A Talon implementation issue distinguishes the agent, requester, approver, and administrator, then lists risks from free-form reviewer fields, permissive defaults, mutable groups, credential revocation, and decision races. Treat that issue as a project report, not a universal platform guarantee. Use its failure cases as tests for any approval design.
Build an immutable approval subject
Create the approval subject before showing the request to anyone. It should contain every value that could change the risk or effect:
approval_subject:
subject_id: "aps_01K..."
action_id: "act_01K..."
tool: "billing.refund"
arguments_digest: "sha256:..."
target:
type: "invoice"
id: "inv_4821"
expected_version: 17
requester_principal: "usr_174"
initiating_agent_chain:
- "agent_support_triage"
- "agent_refund_planner"
agent_release: "support-2026-08-26.3"
risk_class: "financial_high"
policy_id: "refund-separation"
policy_version: 12
required_approvals: 2
eligibility_predicates:
- "role:finance_approver"
- "not:requester"
- "not:initiating_agent_operator"
- "distinct:business_unit"
expires_at: "2026-08-26T14:30:00Z"
Canonicalize the tool name, arguments, target, and expected resource version before hashing. Store the full protected payload in the product database, then display a human-readable projection from that same record. Do not let the model write the final reviewer list, risk class, policy ID, or exclusion predicates.
The subject ID belongs in every approval response, execution grant, audit event, and effect receipt. Editing the tool arguments, target, policy, required quorum, or expected version creates a new subject and invalidates prior decisions. A cosmetic note can remain outside the digest only when it cannot alter execution or an approver's understanding.
The Fishhawk approval policy proposal gives concrete practitioner language for quorum counts, not predicates, permission and group eligibility, requester exclusion, decision-time checks, and fail-closed behavior. Its design is project-specific, but the predicate model exposes the information a portable approval subject needs.
Enforce independent identity and quorum
Resolve approvers from an authoritative identity source at decision time. A signed-in browser session can identify the human making the decision, while the server checks current employment status, tenant, role, group membership, authentication strength, and suspension state. Workflow metadata may suggest routing, but it must not grant eligibility.
Model every relevant principal separately:
| Principal | Required evidence | Typical exclusion |
|---|---|---|
| Requester | Current user or service identity | Cannot approve their own proposal |
| Agent operator | Owner or operator responsible for the initiating release | Excluded when policy requires organizational independence |
| Delegated agent | Stable runtime identity and parent chain | Cannot become a synthetic approver |
| Approver | Human identity, current role, tenant, and authentication event | Must satisfy every policy predicate |
| Executor | Workload identity consuming the grant | Cannot choose or alter approvers |
| Policy administrator | Identity that changed the rule | May require a separate change-control path |
Use stable identity IDs, not email addresses or display names. Expand groups through the identity provider, then count unique human principals after expansion. Two group memberships held by one person count once. Shared accounts, service principals, and autonomous agents should not satisfy a human quorum unless a policy explicitly defines a separate machine-approval role with independent authority.
Quorum rules need positive and negative predicates. Positive predicates state who may approve, such as role:finance_approver. Negative predicates state conflicts, such as requester, action author, initiating agent operator, same management chain, or same business unit. Evaluate both from product-owned policy.
A directly reviewed clinical governance project issue reported that its no-self-approval claim depended on operators keeping two configured principal sets disjoint, with no structural check in code. Configuration alone leaves self-approval structurally possible. Reject a policy that permits an identity to occupy conflicting roles when the action is created, and check again when each decision is recorded.
Issue a single-use execution grant
Approval should produce evidence, then a separate execution grant should authorize the effect. The grant contains no broad session authority. Scope it to the subject digest, tool, target, expected resource version, tenant, executor identity, expiry, and one consumption attempt.
function authorize_and_execute(subject_id):
subject = load_subject_for_update(subject_id)
assert subject.state == "approved"
assert now() < subject.expires_at
policy = load_current_policy(subject.policy_id)
identities = resolve_current_identities(subject)
resource = load_current_resource(subject.target)
assert policy.version == subject.policy_version
assert resource.version == subject.target.expected_version
assert quorum_is_valid(subject, identities, policy)
grant = mint_single_use_grant(subject)
mark_grant_consumed(grant.id)
receipt = execute_idempotently(subject.action_id, subject.tool, subject.arguments)
commit_execution(subject.id, grant.id, receipt)
Put grant consumption and dispatch admission behind one transaction or durable workflow boundary. The external tool call may still have an uncertain outcome after a timeout. Reconcile that outcome with an idempotency key or provider receipt before retrying. Do not mint a second grant merely because the client lost the response.
The executor must reject a grant when the subject digest, tenant, tool, target, resource version, policy version, executor identity, or expiry differs. The agent runtime receives only the decision and scoped grant reference. It never receives an approver credential or a reusable human session token.
Handle drift, denial, and recovery
Waiting is the normal state of an approval workflow. Design the state machine for change:
- Approver loses eligibility: Keep the historical decision for evidence, remove it from the current quorum, and notify the workflow owner. Do not silently substitute another identity.
- Policy changes: Invalidate the pending subject when the new policy affects risk, eligibility, quorum, or exclusions. Recreate the request under the new version.
- Target changes: Compare the current resource version with the frozen expected version. Require a new subject if the approved effect would apply to different state.
- Quorum returns conflicting decisions: Follow an explicit policy. High-impact actions should treat any required-role denial as terminal unless a separate appeal path exists.
- Identity provider is unavailable: Fail closed for approval and execution. Preserve the pending request and show the operator which dependency blocked progress.
- Evidence write fails: Do not execute an action whose policy requires an approval receipt. Queue a bounded retry or move the workflow to operator review.
- Execution times out: Query the downstream system by the action's idempotency key. Record confirmed success, confirmed failure, or unknown. Unknown does not authorize replay.
- Grant expires after approval: Recheck policy, identities, resource state, and quorum before issuing a replacement. Never extend the old token in place.
OWASP MCP02 recommends fail-closed elevation, bounded scope, immutable audit trails, and approval for sensitive operations. Use those principles at both decision time and execution time. A clean approval UI cannot compensate for an executor that accepts stale or overbroad authority.
Verify separation with negative tests
A positive test proving that two managers can approve a request is weak evidence. The release gate should prove that forbidden combinations cannot reach the effect boundary.
Run at least these cases:
- The requester attempts to approve through another browser session.
- The owner of the initiating agent release also belongs to an allowed approver group.
- One person belongs to both groups required by a two-role quorum.
- A child agent creates an approval request that names its parent operator as reviewer.
- Workflow input tries to replace the server-selected approver policy.
- An approver leaves the required group after approving but before execution.
- The policy version changes while one of two approvals is pending.
- The target resource changes after approval.
- The tool arguments change by one field while retaining the old subject ID.
- A consumed grant is submitted again by the same worker and by a different worker.
- The identity provider, policy store, and evidence store fail separately.
- The external effect succeeds but the response times out.
- A denied request is resumed from an old checkpoint.
- Audit access is used to search for a raw approver credential or session token.
For each case, assert three outputs: no unauthorized effect occurred, the workflow entered a typed terminal or recoverable state, and the evidence record names the subject, current policy, evaluated identities, predicate results, and reason code. Keep sensitive payloads and credentials out of that record. Store digests, stable IDs, policy references, decisions, and downstream receipts.
Avoid shortcuts that collapse the control
Several shortcuts make the feature look complete while preserving the original risk:
- A free-form
reviewerfield lets the workflow choose its own control. - A role name stored only in the checkpoint ignores current identity state.
- Two approval clicks from one person do not create a quorum.
- A planner agent and executor agent using the same broad service account are not independent.
- Auto-approval middleware defeats the control when enabled on a sensitive route. The Microsoft approval guide documents auto-approval as an implementation option, so keep it outside routes whose policy requires separation.
- An approval boolean without a subject digest can authorize changed arguments.
- A long-lived grant can escape the approved run and authorize later work.
- An audit log written after execution cannot block an invalid decision.
Current articles from Kovrr and Security Boulevard explain trust tiers, planning and execution separation, maker-checker controls, and monitoring. Enforce those boundaries in the product's identity, policy, and execution gateways.
Start with one high-impact action
Choose one production action such as a refund, permission change, data export, deployment, or destructive deletion. Write down its requester, initiating agent chain, independent approver roles, negative predicates, quorum, policy version, target version, grant scope, expiry, and receipt. Build the approval subject and the executor's rejection checks before changing the UI.
Then run the fourteen negative tests against the real execution gateway. Ship AI agent separation of duties only when a stale, self-approved, collusive, altered, or replayed request cannot reach the effect and the evidence record explains exactly why.
References
- NIST SP 800-53 Rev. 5 supports the AC-5 separation-of-duties control and enforcement through access authorizations.
- OWASP MCP02:2025, Privilege Escalation via Scope Creep supports unique agent identities, least privilege, just-in-time elevation, approval gates, fail-closed behavior, and immutable attribution.
- Microsoft Agent Framework, function tools with human-in-the-loop approvals supports approval-required tools, bound tool arguments, same-session resumption, queued requests, and the documented auto-approval option.
- Fishhawk issue 1705 provides a practitioner implementation proposal for quorum, eligibility predicates, requester exclusion, decision-time checks, and closed failure.
- Talon issue 428 provides a practitioner implementation proposal separating requester, agent, approver, and administrator identity while addressing mutable groups, credential lifecycle, and races.
- Clinical AI Governance Platform issue 15 provides an author report of a no-self-approval guarantee that depended on configuration rather than structural enforcement.
- Kovrr, Multi-Agent AI Systems and Separation of Duties supports current terminology around trust tiers, composed permissions, plan-versus-execute separation, and delegation paths.
- Security Boulevard, Separation of Duties for AI Agent Workflows Explained supports maker-checker framing, separate planning, execution, approval, immutable logging, and monitored override.