Approvals inbox
The Approvals inbox screen lets you read the requests that wait for a human decision (tool calls, run steps, promotions, break glass) and approve, reject or cancel them. Portal path: /agents/approvals. Approvers (tenant administrators, security officers, developers, business owners) decide the requests; the requester can only cancel them. The screen is drawn by portal/src/screens/agents.tsx.
What the screen shows
- Approval requests table, read with
GET /v1/approvals(50 rows per call, cursor paging). Before it answers, the Control Plane marks the requests whose deadline passed as expired. Columns: Id, Action, Resource type, Resource id, Requester, Reason, Status, Created. - Filter Status (Pending, Approved, Rejected, Cancelled,
expired, or all), then Filter. - When the list is empty: Nothing waiting for approval, with the hint Requests appear here when a policy requires a human decision. Requests are created by the platform (a policy obligation, a tool marked
requires_approval, a promotion toactiveorapproved); this screen has no create button. - A click on a row, or the row menu View details, opens a side panel with the request read with
GET /v1/approvals/{approval_id}: its timeline when it has one, then the full JSON (payload, quorum, approver roles, decisions).
Actions
The row actions show only while the request is pending.
| Button or action | What it does | API call | Roles |
|---|---|---|---|
| Refresh (icon) | Reloads the list. | GET /v1/approvals | Any tenant member |
| Filter | Applies the Status filter. | GET /v1/approvals with status | Any tenant member |
| Row click or row menu View details | Opens the detail panel. | GET /v1/approvals/{approval_id} | Any tenant member |
| Row menu Approve | Opens a form with an optional Comment, then records an approval. The request becomes approved when the number of approvals reaches its quorum. | POST /v1/approvals/{approval_id}/decide | Roles of approver_roles, by default tenant_admin, security_officer, developer, business_owner; never the requester |
| Row menu Reject | Opens a form with a required Comment, then rejects the request at once. | POST /v1/approvals/{approval_id}/decide | Same as Approve |
| Row menu Cancel | Opens a form with a required Reason (3 to 2000 characters), then withdraws the request. | POST /v1/approvals/{approval_id}/cancel | The requester, tenant_admin, security_officer |
| Retry (error state) | Sends the failed read again. | Same call | Same roles |
The decision forms send this body (decision is approve or reject):
{"decision": "reject", "comment": "Amount above the delegation of the requester"}
Roles
The portal hides no action; the rules live in services/control-plane/src/eai/domains/approvals/service.py:
- Separation of duties: the requester, and every user listed in
payload.excluded_approvers, cannot decide the request (separation_of_duties, 403). - Approver roles: the request may set its own
approver_roles; otherwiseDEFAULT_APPROVER_ROLESapplies (tenant_admin,security_officer,developer,business_owner).platform_adminpasses every role check. - One decision per person: a second decision by the same approver is refused (
already_decided). - Quorum: one rejection closes the request; approvals are counted until the
quorum(1 to 10) is reached. - Cancel: the requester, or a member of
CANCELLERS(tenant_admin,security_officer).
Every decision and cancellation is written to the audit log.
Typical tasks
Approve a pending request
- List what waits. Only pending requests can be decided. Choose Pending in Status and click Filter. Expected result: the pending requests, newest first. If it fails: Nothing waiting for approval means nothing is pending for the tenant.
- Read the request. The payload tells what will run if you approve. Click the row and read
action,resource_type,payloadandquorumin the panel. Expected result: you know the action and its impact. If it fails: when the panel shows an error, click Retry. - Approve it. Your approval is counted toward the quorum. In the row menu, click Approve, add a Comment, then click Approve. Expected result: the toast Done; the status becomes Approved, or stays Pending while the quorum is not reached. If it fails:
separation_of_dutiesmeans you are the requester; ask another approver.
Reject a request with a reason
- Open the form. A rejection must be explained to the requester and to the auditors. In the row menu, click Reject. Expected result: a form with Comment. If it fails: the action is missing when the request is no longer pending.
- Write the comment and confirm. The comment is stored with the decision. Type the reason and click Reject. Expected result: the toast Done and the status Rejected; a waiting run receives the rejection. If it fails: Required under the field means the comment is empty.
Withdraw your own request
- Cancel it. A request that is no longer needed must not stay in the inbox of the approvers. In the row menu, click Cancel, type a Reason of at least 3 characters, then click Cancel. Expected result: the toast Done and the status Cancelled. If it fails:
forbidden(403) means you are neither the requester nortenant_adminnorsecurity_officer.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
separation_of_duties (403): "Separation of duties: the requester cannot approve their own request." | You created the request, or you are listed in excluded_approvers. | Another approver must decide. |
forbidden (403) with required_roles | Your roles are not in the approver roles of the request. | Ask an approver who holds one of the listed roles. |
already_decided (409): "You already decided on this approval." | You already recorded a decision on this request. | Wait for the other approvers to reach the quorum. |
approval_closed (409): "The approval is already ..." | The request was decided, cancelled or expired meanwhile. | Click Refresh; a new request is needed to retry the action. |
| Status stays Pending after Approve | The quorum asks for more approvals. | Read quorum in the detail panel and ask other approvers. |
validation_error (422) on Cancel | The reason is shorter than 3 characters. | Write a longer reason. |