Gateway Approval Attribution
Record who approved a gateway request and bind an approval to a specific operator
Every resolved gateway approval records which operator approved it, and a request can optionally be bound to a specific reviewer so only they can resolve it.
Quick Start
- Approve — attribution is automatic
The gateway captures the resolving operator on the audit trail. No extra code is needed on the agent side.
python from praisonaiagents import Agent agent = Agent( name="Deployer", instructions="Deploy safely", approval=True, ) agent.start("Deploy the release candidate to prod") - Bind a request to specific reviewers
Pass
authorized_reviewersso only listed operators can resolve the request.python from praisonaiagents.approval import ApprovalRequest request = ApprovalRequest( tool_name="deploy", arguments={"target": "prod"}, risk_level="high", agent_name="deployer", authorized_reviewers=["operator:alice", "operator:oncall"], )
How Resolver Identity Is Captured
The POST /approval/resolve endpoint derives a non-secret operator identity from the authenticated request, in this precedence order.
| Order | Source | Identity string |
|---|---|---|
| 1 | X-Operator-Id header (trimmed to 64 chars) | operator:<handle> |
| 2 | Short SHA-256 digest of the bearer token | operator:<12-hex> |
| 3 | Client IP | operator:<ip> |
| 4 | Fallback | gateway |
Set X-Operator-Id from a proxy or SSO layer so operators sharing one gateway token still get distinct audit attribution. Holders of the same token collapse to one token-digest identity by design.
curl -X POST -H "Authorization: Bearer $OPS_TOKEN" \
-H "X-Operator-Id: alice" \
-H "Content-Type: application/json" \
-d '{"request_id": "apr-1a2b3c4d", "approved": true}' \
http://localhost:8080/approval/resolveThe identity flows into Resolution.resolver and is recorded on the audit trail and the allow-always grant. When no resolver is supplied, attribution falls back to the legacy "gateway" / "gateway:human" constant.
Per-request Reviewer Custody
Bind an approval to specific reviewers so only they can resolve it.
| Behaviour | Detail |
|---|---|
| Unauthorised resolver | Receives 404 with {"error": "Request not found, already resolved, or resolver not authorised"} and the request stays pending |
| First-answer-wins | Preserved between authorised reviewers via the atomic pop |
| Durability | The binding survives a gateway restart (persisted and rehydrated by the SQLite store) |
Defence in depth with the core SDK. Since PraisonAI #4950, the core ApprovalRegistry also honours ApprovalRequest.liveness at the resolution boundary. A gateway request that binds liveness is re-checked twice: once by the gateway resolve() boundary and once by the core registry before persisting a grant. Backends that bypass the gateway still get the core fail-closed guarantee automatically. See Approval Protocol → Live-Authority Binding.
Best Practices
Stamp X-Operator-Id at your proxy or SSO layer
Operators sharing one gateway token collapse to a single token-digest identity. Stamp X-Operator-Id per user upstream so each decision is attributed to an individual.
Use reviewer custody for high-risk requests
The APPROVALS scope is coarse — any holder may resolve any request. Bind high-risk requests to named reviewers with authorized_reviewers.
Read the approver field in audit reports
For gateway-resolved requests, approver now carries the real principal, so reports can answer “who approved this exec?”.